package apps

import (
	"strings"

	"github.com/kataras/iris/v12"
)

// Switch returns a new Application
// with the sole purpose of routing the
// matched Applications through the "provided cases".
//
// The cases are filtered in order of their registration.
//
// Example Code:
//
//	switcher := Switch(Hosts{
//		"mydomain.com": app,
//		"test.mydomain.com": testSubdomainApp,
//		"otherdomain.com": "appName",
//	})
//	switcher.Listen(":80")
//
// Note that this is NOT an alternative for a load balancer.
// The filters are executed by registration order and a matched Application
// handles the request, that's all it does.
//
// The returned Switch Iris Application can register routes that will run
// when neither of the registered Applications is responsible
// to handle the incoming request against the provided filters.
// The returned Switch Iris Application can also register custom error code handlers,
// e.g. to inject the 404 on not responsible Application was found.
// It can also be wrapped with its `WrapRouter` method,
// which is really useful for logging and statistics.
//
// Wrap with the `Join` slice to pass
// more than one provider at the same time.
func Switch(provider SwitchProvider, options ...SwitchOption) *iris.Application {
	cases := provider.GetSwitchCases()
	if len(cases) == 0 {
		panic("iris: switch: empty cases")
	}

	var friendlyAddrs []string
	if fp, ok := provider.(FriendlyNameProvider); ok {
		if friendlyName := fp.GetFriendlyName(); friendlyName != "" {
			friendlyAddrs = append(friendlyAddrs, friendlyName)
		}
	}

	opts := DefaultSwitchOptions()
	for _, opt := range options {
		if opt == nil {
			continue
		}

		opt.Apply(&opts)
	}

	app := iris.New()
	// Try to build the cases apps on app.Build/Listen/Run so
	// end-developers don't worry about it.
	app.OnBuild = func() error {
		for _, c := range cases {
			if err := c.App.Build(); err != nil {
				return err
			}
		}
		return nil
	}
	// If we have a request to support
	// middlewares in that switcher app then
	// we can use app.Get("{p:path}"...) instead.
	app.UseRouter(func(ctx iris.Context) {
		for _, c := range cases {
			if c.Filter(ctx) {
				w := ctx.ResponseWriter()
				r := ctx.Request()

				for _, reqMod := range opts.RequestModifiers {
					reqMod(r)
				}

				c.App.ServeHTTP(w, r)

				// if c.App.Downgraded() {
				// 	c.App.ServeHTTP(w, r)
				// } else {
				// Note(@kataras): don't ever try something like that;
				// the context pool is the switcher's one.
				// 	ctx.SetApplication(c.App)
				// 	c.App.ServeHTTPC(ctx)
				// 	ctx.SetApplication(app)
				// }
				return
			}
		}

		// let the "switch app" handle it or fire a custom 404 error page,
		// next is the switch app's router.
		ctx.Next()
	})

	// Configure the switcher's supervisor.
	app.ConfigureHost(func(su *iris.Supervisor) {
		if len(friendlyAddrs) > 0 {
			su.FriendlyAddr = strings.Join(friendlyAddrs, ", ")
		}
	})
	return app
}

type (
	// SwitchCase contains the filter
	// and the matched Application instance.
	SwitchCase struct {
		Filter iris.Filter       // Filter runs against the Switcher.
		App    *iris.Application // App is the main target application responsible to handle the request.
	}

	// A SwitchProvider should return the switch cases.
	// It's an interface instead of a direct slice because
	// we want to make available different type of structures
	// without wrapping.
	SwitchProvider interface {
		GetSwitchCases() []SwitchCase
	}

	// FriendlyNameProvider can be optionally implemented by providers
	// to customize the Switcher's Supervisor.FriendlyAddr field (Startup log).
	FriendlyNameProvider interface {
		GetFriendlyName() string
	}

	// Join returns a new slice which joins different type of switch cases.
	Join []SwitchProvider
)

var _ SwitchProvider = SwitchCase{}

// GetSwitchCases completes the SwitchProvider, it returns itself.
func (sc SwitchCase) GetSwitchCases() []SwitchCase {
	return []SwitchCase{sc}
}

var _ SwitchProvider = Join{}

// GetSwitchCases completes the switch provider.
func (j Join) GetSwitchCases() (cases []SwitchCase) {
	for _, p := range j {
		if p == nil {
			continue
		}

		cases = append(cases, p.GetSwitchCases()...)
	}

	return
}
