package apps

import (
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strings"

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

type (
	// Host holds the pattern for the SwitchCase filter
	// and the Target host or application.
	Host struct {
		// Pattern is the incoming host matcher regexp or a literal.
		Pattern string
		// Target is the target Host that incoming requests will be redirected on pattern match
		// or an Application's Name that will handle the incoming request matched the Pattern.
		Target interface{} // It was a string in my initial design but let's do that interface{}, we may support more types here in the future, until generics are in, keep it interface{}.
	}
	// Hosts is a switch provider.
	// It can be used as input argument to the `Switch` function
	// to map host to existing Iris Application instances, e.g.
	// { "www.mydomain.com": "mydomainApp" } .
	// It can accept regexp as a host too, e.g.
	// { "^my.*$": "mydomainApp" } .
	Hosts []Host

	// Good by we need order and map can't provide it for us
	//  (e.g. "fallback" regexp }
	// Hosts map[string]*iris.Application
)

var _ SwitchProvider = Hosts{}

// GetSwitchCases completes the SwitchProvider.
// It returns a slice of SwitchCase which
// if passed on `Switch` function, they act
// as a router between matched domains and subdomains
// between existing Iris Applications.
func (hosts Hosts) GetSwitchCases() []SwitchCase {
	cases := make([]SwitchCase, 0, len(hosts))

	for _, host := range hosts {
		cases = append(cases, SwitchCase{
			Filter: hostFilter(host.Pattern),
			App:    hostApp(host),
		})
	}

	return cases
}

// GetFriendlyName implements the FriendlyNameProvider.
func (hosts Hosts) GetFriendlyName() string {
	var patterns []string
	for _, host := range hosts {
		if strings.TrimSpace(host.Pattern) != "" {
			patterns = append(patterns, host.Pattern)
		}
	}

	return strings.Join(patterns, ", ")
}

func hostApp(host Host) *iris.Application {
	if host.Target == nil {
		return nil
	}

	switch target := host.Target.(type) {
	case context.Application:
		return target.(*iris.Application)
	case string:
		// Check if the given target is an application name, if so
		// we must not redirect (loop) we must serve the request
		// using that app.
		if targetApp, ok := context.GetApplication(target); ok {
			// It's always iris.Application so we are totally safe here.
			return targetApp.(*iris.Application)
		}
		// If it's a real host, warn the user of invalid input.
		u, err := url.Parse(target)
		if err == nil && u.IsAbs() {
			// remember, we redirect hosts, not full URLs here.
			panic(fmt.Sprintf(`iris: switch: hosts: invalid target host: "%s"`, target))
		}

		if regex := regexp.MustCompile(host.Pattern); regex.MatchString(target) {
			panic(fmt.Sprintf(`iris: switch: hosts: loop detected between expression: "%s" and target host: "%s"`, host.Pattern, host.Target))
		}

		return newHostRedirectApp(target, HostsRedirectCode)
	default:
		panic(fmt.Sprintf("iris: switch: hosts: invalid target type: %T", target))
	}
}

func hostFilter(expr string) iris.Filter {
	regex := regexp.MustCompile(expr)
	return func(ctx iris.Context) bool {
		return regex.MatchString(ctx.Host())
	}
}

// HostsRedirectCode is the default status code is used
// to redirect a matching host to a url.
var HostsRedirectCode = iris.StatusMovedPermanently

func newHostRedirectApp(targetHost string, code int) *iris.Application {
	app := iris.New()
	app.Downgrade(func(w http.ResponseWriter, r *http.Request) {
		if targetHost == context.GetHost(r) {
			// Note(@kataras):
			// this should never happen as the HostsRedirect
			// carefully checks if the expression already matched the "redirectTo"
			// to avoid the redirect loops at all.
			// iris: switch: hosts redirect: loop detected between expression: "^my.*$" and target host: "mydomain.com"
			http.Error(w, iris.StatusText(iris.StatusTooManyRequests), iris.StatusTooManyRequests)
			return
		}

		r.Host = targetHost
		r.URL.Host = targetHost
		// r.URL.User = nil
		http.Redirect(w, r, r.URL.String(), code)
	})
	return app
}
