package router

import (
	"fmt"
	"net/http"
	"sort"
	"strings"
	"time"

	"github.com/kataras/iris/v12/context"
	"github.com/kataras/iris/v12/core/errgroup"
	"github.com/kataras/iris/v12/core/netutil"
	macroHandler "github.com/kataras/iris/v12/macro/handler"

	"github.com/kataras/golog"
	"github.com/kataras/pio"
)

type (
	// RequestHandler the middle man between acquiring a context and releasing it.
	// By-default is the router algorithm.
	RequestHandler interface {
		// Note: A different interface in order  to hide the rest of the implementation.
		// We only need the `FireErrorCode` to be accessible through the Iris application (see `iris.go#Build`)
		HTTPErrorHandler

		// HandleRequest should handle the request based on the Context.
		HandleRequest(ctx *context.Context)
		// Build should builds the handler, it's being called on router's BuildRouter.
		Build(provider RoutesProvider) error
		// RouteExists reports whether a particular route exists.
		RouteExists(ctx *context.Context, method, path string) bool
	}

	// HTTPErrorHandler should contain a method `FireErrorCode` which
	// handles http unsuccessful status codes.
	HTTPErrorHandler interface {
		// FireErrorCode should send an error response to the client based
		// on the given context's response status code.
		FireErrorCode(ctx *context.Context)
	}
)

type routerHandler struct {
	// Config.
	disablePathCorrection            bool
	disablePathCorrectionRedirection bool
	fireMethodNotAllowed             bool
	enablePathIntelligence           bool
	forceLowercaseRouting            bool
	//
	logger *golog.Logger

	trees      []*trie
	errorTrees []*trie

	hosts                bool             // true if at least one route contains a Subdomain.
	errorHosts           bool             // true if error handlers are registered to at least one Subdomain.
	errorDefaultHandlers context.Handlers // the main handler(s) for default error code handlers, when not registered directly by the end-developer.
}

var _ RequestHandler = (*routerHandler)(nil)
var _ HTTPErrorHandler = (*routerHandler)(nil)

// NewDefaultHandler returns the handler which is responsible
// to map the request with a route (aka mux implementation).
func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logger) RequestHandler {
	return &routerHandler{
		disablePathCorrection:            config.GetDisablePathCorrection(),
		disablePathCorrectionRedirection: config.GetDisablePathCorrectionRedirection(),
		fireMethodNotAllowed:             config.GetFireMethodNotAllowed(),
		enablePathIntelligence:           config.GetEnablePathIntelligence(),
		forceLowercaseRouting:            config.GetForceLowercaseRouting(),
		logger:                           logger,
	}
}

func (h *routerHandler) getTree(statusCode int, method, subdomain string) *trie {
	if statusCode > 0 {
		for i := range h.errorTrees {
			t := h.errorTrees[i]
			if t.statusCode == statusCode && t.subdomain == subdomain {
				return t
			}
		}
		return nil
	}

	for i := range h.trees {
		t := h.trees[i]
		if t.method == method && t.subdomain == subdomain {
			return t
		}
	}

	return nil
}

// AddRoute registers a route. See `Router.AddRouteUnsafe`.
func (h *routerHandler) AddRoute(r *Route) error {
	var (
		method     = r.Method
		statusCode = r.StatusCode
		subdomain  = r.Subdomain
		path       = r.Path
		handlers   = r.Handlers
	)

	t := h.getTree(statusCode, method, subdomain)

	if t == nil {
		n := newTrieNode()
		// first time we register a route to this method with this subdomain
		t = &trie{statusCode: statusCode, method: method, subdomain: subdomain, root: n}
		if statusCode > 0 {
			h.errorTrees = append(h.errorTrees, t)
		} else {
			h.trees = append(h.trees, t)
		}
	}

	t.insert(path, r.ReadOnly, handlers)

	return nil
}

// RoutesProvider should be implemented by
// iteral which contains the registered routes.
type RoutesProvider interface { // api builder
	GetRoutes() []*Route
	GetRoute(routeName string) *Route
	// GetRouterFilters returns the app's router filters.
	// Read `UseRouter` for more.
	// The map can be altered before router built.
	GetRouterFilters() map[Party]*Filter
	// GetDefaultErrorMiddleware should return
	// the default error handler middleares.
	GetDefaultErrorMiddleware() context.Handlers
}

func defaultErrorHandler(ctx *context.Context) {
	if ok, err := ctx.GetErrPublic(); ok {
		// If an error is stored and it's not a private one
		// write it to the response body.
		ctx.WriteString(err.Error())
		return
	}
	// Otherwise, write the code's text instead.
	ctx.WriteString(context.StatusText(ctx.GetStatusCode()))
}

func (h *routerHandler) Build(provider RoutesProvider) error {
	h.trees = h.trees[0:0] // reset, inneed when rebuilding.
	h.errorTrees = h.errorTrees[0:0]

	// set the default error code handler, will be fired on error codes
	// that are not handled by a specific handler (On(Any)ErrorCode).
	h.errorDefaultHandlers = append(provider.GetDefaultErrorMiddleware(), defaultErrorHandler)

	rp := errgroup.New("Routes Builder")
	registeredRoutes := provider.GetRoutes()

	// before sort.
	for _, r := range registeredRoutes {
		if r.topLink != nil {
			bindMultiParamTypesHandler(r)
		}
	}

	// sort, subdomains go first.
	sort.Slice(registeredRoutes, func(i, j int) bool {
		first, second := registeredRoutes[i], registeredRoutes[j]
		lsub1 := len(first.Subdomain)
		lsub2 := len(second.Subdomain)

		firstSlashLen := strings.Count(first.Path, "/")
		secondSlashLen := strings.Count(second.Path, "/")

		if lsub1 == lsub2 && first.Method == second.Method {
			if secondSlashLen < firstSlashLen {
				// fixes order when wildcard root is registered before other wildcard paths
				return true
			}

			if secondSlashLen == firstSlashLen {
				// fixes order when static path with the same prefix with a wildcard path
				// is registered after the wildcard path, although this is managed
				// by the low-level node but it couldn't work if we registered a root level wildcard, this fixes it.
				if len(first.tmpl.Params) == 0 {
					return false
				}
				if len(second.tmpl.Params) == 0 {
					return true
				}

				// No don't fix the order by framework's suggestion,
				// let it as it is today; {string} and {path} should be registered before {id} {uint} and e.t.c.
				// see `bindMultiParamTypesHandler` for the reason. Order of registration matters.
			}
		}

		// the rest are handled inside the node
		return lsub1 > lsub2
	})

	noLogCount := 0

	for _, r := range registeredRoutes {
		if r.NoLog {
			noLogCount++
		}

		if h.forceLowercaseRouting {
			// only in that state, keep everything else as end-developer registered.
			r.Path = strings.ToLower(r.Path)
		}

		if r.Subdomain != "" {
			if r.StatusCode > 0 {
				h.errorHosts = true
			} else {
				h.hosts = true
			}
		}

		if r.topLink == nil {
			// build the r.Handlers based on begin and done handlers, if any.
			r.BuildHandlers()

			// the only "bad" with this is if the user made an error
			// on route, it will be stacked shown in this build state
			// and no in the lines of the user's action, they should read
			// the docs better. Or TODO: add a link here in order to help new users.
			if err := h.AddRoute(r); err != nil {
				// node errors:
				rp.Addf("%s: %w", r.String(), err)
				continue
			}
		}
	}

	// TODO: move this and make it easier to read when all cases are, visually, tested.
	if logger := h.logger; logger != nil && logger.Level == golog.DebugLevel && noLogCount < len(registeredRoutes) {
		// group routes by method and print them without the [DBUG] and time info,
		// the route logs are colorful.
		// Note: don't use map, we need to keep registered order, use
		// different slices for each method.

		collect := func(method string) (methodRoutes []*Route) {
			for _, r := range registeredRoutes {
				if r.NoLog {
					continue
				}
				if r.Method == method {
					methodRoutes = append(methodRoutes, r)
				}
			}

			return
		}

		type MethodRoutes struct {
			method string
			routes []*Route
		}

		allMethods := append(AllMethods, []string{MethodNone, ""}...)
		methodRoutes := make([]MethodRoutes, 0, len(allMethods))

		for _, method := range allMethods {
			routes := collect(method)
			if len(routes) > 0 {
				methodRoutes = append(methodRoutes, MethodRoutes{method, routes})
			}
		}

		if n := len(methodRoutes); n > 0 {
			tr := "routes"
			if len(registeredRoutes) == 1 {
				tr = tr[0 : len(tr)-1]
			}

			bckpNewLine := logger.NewLine
			logger.NewLine = false
			debugLevel := golog.Levels[golog.DebugLevel]
			// Replace that in order to not transfer it to the log handler (e.g. json)
			// logger.Debugf("API: %d registered %s (", len(registeredRoutes), tr)
			// with:
			pio.WriteRich(logger.Printer, debugLevel.Title, debugLevel.ColorCode, debugLevel.Style...)
			fmt.Fprintf(logger.Printer, " %s %sAPI: %d registered %s (", time.Now().Format(logger.TimeFormat), logger.Prefix, len(registeredRoutes)-noLogCount, tr)
			//
			logger.NewLine = bckpNewLine

			for i, m := range methodRoutes {
				// @method: @count
				if i > 0 {
					if i == n-1 {
						fmt.Fprint(logger.Printer, " and ")
					} else {
						fmt.Fprint(logger.Printer, ", ")
					}
				}
				if m.method == "" {
					m.method = "ERROR"
				}
				fmt.Fprintf(logger.Printer, "%d ", len(m.routes))
				pio.WriteRich(logger.Printer, m.method, TraceTitleColorCode(m.method))
			}

			fmt.Fprint(logger.Printer, ")\n")
		}

		for i, m := range methodRoutes {
			for _, r := range m.routes {
				r.Trace(logger.Printer, -1)
			}

			if i != len(allMethods)-1 {
				logger.Printer.Write(pio.NewLine)
			}
		}
	}

	return errgroup.Check(rp)
}

func bindMultiParamTypesHandler(r *Route) { // like overlap feature but specifically for path parameters.
	r.BuildHandlers()

	h := r.Handlers[1:] // remove the macro evaluator handler as we manually check below.
	f := macroHandler.MakeFilter(r.tmpl)
	if f == nil {
		return // should never happen, previous checks made to set the top link.
	}

	currentStatusCode := r.StatusCode
	if currentStatusCode == 0 {
		currentStatusCode = http.StatusOK
	}

	decisionHandler := func(ctx *context.Context) {
		// println("core/router/handler.go: decision handler; " + ctx.Path() + " route.Name: " + r.Name + " vs context's " + ctx.GetCurrentRoute().Name())
		currentRoute := ctx.GetCurrentRoute()

		// Different path parameters types in the same path, fallback should registered first e.g. {path} {string},
		// because the handler on this case is executing from last to top.
		if f(ctx) {
			// println("core/router/handler.go: filter for : " + r.Name + " passed")
			ctx.SetCurrentRoute(r.ReadOnly)
			// Note: error handlers will be the same, routes came from the same party,
			// no need to update them.
			ctx.HandlerIndex(0)
			ctx.Do(h)
			return
		}

		ctx.SetCurrentRoute(currentRoute)
		ctx.StatusCode(currentStatusCode)
		ctx.Next()
	}

	r.topLink.builtinBeginHandlers = append(context.Handlers{decisionHandler}, r.topLink.builtinBeginHandlers...)
}

func canHandleSubdomain(ctx *context.Context, subdomain string) bool {
	if subdomain == "" {
		return true
	}

	requestHost := ctx.Host()
	if netutil.IsLoopbackSubdomain(requestHost) {
		// this fixes a bug when listening on
		// 127.0.0.1:8080 for example
		// and have a wildcard subdomain and a route registered to root domain.
		return false // it's not a subdomain, it's something like 127.0.0.1 probably
	}
	// it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty
	if subdomain == SubdomainWildcardIndicator {
		// mydomain.com -> invalid
		// localhost -> invalid
		// sub.mydomain.com -> valid
		// sub.localhost -> valid
		serverHost := ctx.Application().ConfigurationReadOnly().GetVHost()
		if serverHost == requestHost {
			return false // it's not a subdomain, it's a full domain (with .com...)
		}

		dotIdx := strings.IndexByte(requestHost, '.')
		slashIdx := strings.IndexByte(requestHost, '/')
		if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) {
			// if "." was found anywhere but not at the first path segment (host).
		} else {
			return false
		}
		// continue to that, any subdomain is valid.
	} else if !strings.HasPrefix(requestHost, subdomain) { // subdomain contains the dot, e.g. "admin."
		return false
	}

	return true
}

func (h *routerHandler) HandleRequest(ctx *context.Context) {
	method := ctx.Method()
	path := ctx.Path()

	if !h.disablePathCorrection {
		if len(path) > 1 && strings.HasSuffix(path, "/") {
			// Remove trailing slash and client-permanent rule for redirection,
			// if confgiuration allows that and path has an extra slash.

			// update the new path and redirect.
			u := ctx.Request().URL
			// use Trim to ensure there is no open redirect due to two leading slashes
			path = "/" + strings.Trim(path, "/")
			u.Path = path
			if !h.disablePathCorrectionRedirection {
				// do redirect, else continue with the modified path without the last "/".
				url := u.String()

				// Fixes https://github.com/kataras/iris/issues/921
				// This is caused for security reasons, imagine a payment shop,
				// you can't just permantly redirect a POST request, so just 307 (RFC 7231, 6.4.7).
				if method == http.MethodPost || method == http.MethodPut {
					ctx.Redirect(url, http.StatusTemporaryRedirect)
					return
				}

				ctx.Redirect(url, http.StatusMovedPermanently)
				return
			}

		}
	}

	for i := range h.trees {
		t := h.trees[i]
		if method != t.method {
			continue
		}

		if h.hosts && !canHandleSubdomain(ctx, t.subdomain) {
			continue
		}

		n := t.search(path, ctx.Params())
		if n != nil {
			ctx.SetCurrentRoute(n.Route)
			ctx.Do(n.Handlers)
			// found
			return
		}
		// not found or method not allowed.
		break
	}

	if h.fireMethodNotAllowed {
		for i := range h.trees {
			t := h.trees[i]
			// if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not
			// run, therefore performance kept as before.
			if h.subdomainAndPathAndMethodExists(ctx, t, "", path) {
				// RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
				// The response MUST include an Allow header containing a list of valid methods for the requested resource.
				ctx.Header("Allow", t.method)
				ctx.StatusCode(http.StatusMethodNotAllowed)
				return
			}
		}
	}

	if h.enablePathIntelligence && method == http.MethodGet {
		closestPaths := ctx.FindClosest(1)
		if len(closestPaths) > 0 {
			u := ctx.Request().URL
			u.Path = closestPaths[0]
			ctx.Redirect(u.String(), http.StatusMovedPermanently)
			return
		}
	}

	ctx.StatusCode(http.StatusNotFound)
}

func statusCodeSuccessful(statusCode int) bool {
	return !context.StatusCodeNotSuccessful(statusCode)
}

// FireErrorCode handles the response's error response.
// If `Configuration.ResetOnFireErrorCode()` is true
// and the response writer was a recorder one
// then it will try to reset the headers and the body before calling the
// registered (or default) error handler for that error code set by
// `ctx.StatusCode` method.
func (h *routerHandler) FireErrorCode(ctx *context.Context) {
	// On common response writer, always check
	// if we can't reset the body and the body has been filled
	// which means that the status code already sent,
	// then do not fire this custom error code,
	// rel: context/context.go#EndRequest.
	//
	// Note that, this is set to 0 on recorder because it holds the response before sent,
	// so we check their len(Body()) instead, look below.
	if ctx.ResponseWriter().Written() > 0 {
		return
	}

	statusCode := ctx.GetStatusCode() // the response's cached one.

	if ctx.Application().ConfigurationReadOnly().GetResetOnFireErrorCode() /* could be an argument too but we must not break the method */ {
		// if we can reset the body, probably manual call of `Application.FireErrorCode`.
		if w, ok := ctx.IsRecording(); ok {
			if statusCodeSuccessful(w.StatusCode()) { // if not an error status code
				w.WriteHeader(statusCode) // then set it manually here, otherwise it should be set via ctx.StatusCode(...)
			}
			// reset if previous content and it's recorder, keep the status code.
			w.ClearHeaders()
			w.ResetBody()

			if cw, ok := w.ResponseWriter.(*context.CompressResponseWriter); ok {
				// recorder wraps a compress writer.
				cw.Disabled = true
			}
		} else if w, ok := ctx.ResponseWriter().(*context.CompressResponseWriter); ok {
			// reset and disable the gzip in order to be an expected form of http error result
			w.Disabled = true
		}
	} else {
		// check if a body already set (the error response is handled by the handler itself,
		// see `Context.EndRequest`)
		if w, ok := ctx.IsRecording(); ok {
			if len(w.Body()) > 0 {
				return
			}
		}
	}

	for i := range h.errorTrees {
		t := h.errorTrees[i]

		if statusCode != t.statusCode {
			continue
		}

		if h.errorHosts && !canHandleSubdomain(ctx, t.subdomain) {
			continue
		}

		n := t.search(ctx.Path(), ctx.Params())
		if n == nil {
			// try to take the root's one.
			n = t.root.getChild(pathSep)
		}

		if n != nil {
			// Note: handlers can contain macro filters here,
			// they are registered as error handlers, see macro/handler.go#42.

			// fmt.Println("Error Handlers")
			// for _, h := range n.Handlers {

			// 	f, l := context.HandlerFileLine(h)
			// 	fmt.Printf("%s: %s:%d\n", ctx.Path(), f, l)
			// }

			// fire this http status code's error handlers chain.

			// ctx.StopExecution() // not uncomment this, is here to remember why to.
			// note for me: I don't stopping the execution of the other handlers
			// because may the user want to add a fallback error code
			// i.e
			// users := app.Party("/users")
			// users.Done(func(ctx *context.Context){ if ctx.StatusCode() == 400 { /*  custom error code for /users */ }})

			// use .HandlerIndex
			// that sets the current handler index to zero
			// in order to:
			// ignore previous runs that may changed the handler index,
			// via ctx.Next or ctx.StopExecution, if any.
			//
			// use .Do
			// that overrides the existing handlers and sets and runs these error handlers.
			// in order to:
			// ignore the route's after-handlers, if any.
			ctx.SetCurrentRoute(n.Route)
			// Should work with:
			// Manual call of ctx.Application().FireErrorCode(ctx) (handlers length > 0)
			// And on `ctx.SetStatusCode`: Context -> EndRequest -> FireErrorCode (handlers length > 0)
			// And on router: HandleRequest -> SetStatusCode -> Context ->
			//                EndRequest -> FireErrorCode (handlers' length is always 0)
			ctx.HandlerIndex(0)
			ctx.Do(n.Handlers)
			return
		}

		break
	}

	// not error handler found,
	// see if failed with a stored error, and if so
	// then render it, otherwise write a default message.
	ctx.Do(h.errorDefaultHandlers)
}

func (h *routerHandler) subdomainAndPathAndMethodExists(ctx *context.Context, t *trie, method, path string) bool {
	if method != "" && method != t.method {
		return false
	}

	if h.hosts && t.subdomain != "" {
		requestHost := ctx.Host()
		if netutil.IsLoopbackSubdomain(requestHost) {
			// this fixes a bug when listening on
			// 127.0.0.1:8080 for example
			// and have a wildcard subdomain and a route registered to root domain.
			return false // it's not a subdomain, it's something like 127.0.0.1 probably
		}
		// it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty
		if t.subdomain == SubdomainWildcardIndicator {
			// mydomain.com -> invalid
			// localhost -> invalid
			// sub.mydomain.com -> valid
			// sub.localhost -> valid
			serverHost := ctx.Application().ConfigurationReadOnly().GetVHost()
			if serverHost == requestHost {
				return false // it's not a subdomain, it's a full domain (with .com...)
			}

			dotIdx := strings.IndexByte(requestHost, '.')
			slashIdx := strings.IndexByte(requestHost, '/')
			if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) {
				// if "." was found anywhere but not at the first path segment (host).
			} else {
				return false
			}
			// continue to that, any subdomain is valid.
		} else if !strings.HasPrefix(requestHost, t.subdomain) { // t.subdomain contains the dot.
			return false
		}
	}

	n := t.search(path, ctx.Params())
	return n != nil
}

// RouteExists reports whether a particular route exists
// It will search from the current subdomain of context's host, if not inside the root domain.
func (h *routerHandler) RouteExists(ctx *context.Context, method, path string) bool {
	for i := range h.trees {
		t := h.trees[i]
		if h.subdomainAndPathAndMethodExists(ctx, t, method, path) {
			return true
		}
	}

	return false
}
