package router

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

	"github.com/owncloud/ocis/v2/ocis-pkg/log"
	"github.com/owncloud/ocis/v2/ocis-pkg/registry"
	"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
	"github.com/owncloud/ocis/v2/services/proxy/pkg/proxy/policy"
	"go-micro.dev/v4/selector"
)

type routingInfoCtxKey struct{}

var noInfo = RoutingInfo{}

// Middleware returns a HTTP middleware containing the router.
func Middleware(policySelector *config.PolicySelector, policies []config.Policy, logger log.Logger) func(http.Handler) http.Handler {
	router := New(policySelector, policies, logger)
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ri, ok := router.Route(r)
			if !ok {
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			next.ServeHTTP(w, r.WithContext(SetRoutingInfo(r.Context(), ri)))
		})
	}
}

// New creates a new request router.
// It initializes the routes before returning the router.
func New(policySelector *config.PolicySelector, policies []config.Policy, logger log.Logger) Router {
	if policySelector == nil {
		firstPolicy := policies[0].Name
		logger.Warn().Str("policy", firstPolicy).Msg("policy-selector not configured. Will always use first policy")
		policySelector = &config.PolicySelector{
			Static: &config.StaticSelectorConf{
				Policy: firstPolicy,
			},
		}
	}

	logger.Debug().
		Interface("selector_config", policySelector).
		Msg("loading policy-selector")

	selector, err := policy.LoadSelector(policySelector)
	if err != nil {
		logger.Fatal().Err(err).Msg("Could not load policy-selector")
	}

	r := Router{
		logger:         logger,
		directors:      make(map[string]map[config.RouteType]map[string][]RoutingInfo),
		policySelector: selector,
	}
	for _, pol := range policies {
		for _, route := range pol.Routes {
			logger.Debug().Str("fwd: ", route.Endpoint)

			if route.Backend == "" && route.Service == "" {
				logger.Fatal().Interface("route", route).Msg("neither Backend nor Service is set")
			}
			uri, err2 := url.Parse(route.Backend)
			if err2 != nil {
				logger.
					Fatal(). // fail early on misconfiguration
					Err(err2).
					Str("backend", route.Backend).
					Msg("malformed url")
			}

			// here the backend is used as a uri
			r.addHost(pol.Name, uri, route)
		}
	}
	return r
}

// RoutingInfo contains the proxy director and some information about the route.
type RoutingInfo struct {
	director    func(*http.Request)
	endpoint    string
	unprotected bool
}

// Director returns the proxy director.
func (r RoutingInfo) Director() func(*http.Request) {
	return r.director
}

// IsRouteUnprotected returns true if the route doesn't need to be authenticated.
func (r RoutingInfo) IsRouteUnprotected() bool {
	return r.unprotected
}

// Router handles the routing of HTTP requests according to the given policies.
type Router struct {
	logger         log.Logger
	directors      map[string]map[config.RouteType]map[string][]RoutingInfo
	policySelector policy.Selector
}

func (rt Router) addHost(policy string, target *url.URL, route config.Route) {
	targetQuery := target.RawQuery
	if rt.directors[policy] == nil {
		rt.directors[policy] = make(map[config.RouteType]map[string][]RoutingInfo)
	}
	routeType := config.DefaultRouteType
	if route.Type != "" {
		routeType = route.Type
	}
	if rt.directors[policy][routeType] == nil {
		rt.directors[policy][routeType] = make(map[string][]RoutingInfo)
	}
	if rt.directors[policy][routeType][route.Method] == nil {
		rt.directors[policy][routeType][route.Method] = make([]RoutingInfo, 0)
	}

	reg := registry.GetRegistry()
	sel := selector.NewSelector(selector.Registry(reg))

	rt.directors[policy][routeType][route.Method] = append(rt.directors[policy][routeType][route.Method], RoutingInfo{
		endpoint:    route.Endpoint,
		unprotected: route.Unprotected,
		director: func(req *http.Request) {
			if route.Service != "" {
				// select next node
				next, err := sel.Select(route.Service)
				if err != nil {
					rt.logger.Error().Err(err).
						Str("service", route.Service).
						Msg("could not select service from the registry")
					return // TODO error? fallback to target.Host & Scheme?
				}
				node, err := next()
				if err != nil {
					rt.logger.Error().Err(err).
						Str("service", route.Service).
						Msg("could not select next node")
					return // TODO error? fallback to target.Host & Scheme?
				}
				req.URL.Host = node.Address
				req.URL.Scheme = node.Metadata["protocol"] // TODO check property exists?

			} else {
				req.URL.Host = target.Host
				req.URL.Scheme = target.Scheme
			}

			// Apache deployments host addresses need to match on req.Host and req.URL.Host
			// see https://stackoverflow.com/questions/34745654/golang-reverseproxy-with-apache2-sni-hostname-error
			if route.ApacheVHost {
				req.Host = target.Host
			}

			req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
			if targetQuery == "" || req.URL.RawQuery == "" {
				req.URL.RawQuery = targetQuery + req.URL.RawQuery
			} else {
				req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
			}
			if _, ok := req.Header["User-Agent"]; !ok {
				// explicitly disable User-Agent so it's not set to default value
				req.Header.Set("User-Agent", "")
			}
		},
	})
}

// Route is evaluating the policies on the request and returns the RoutingInfo if successful.
func (rt Router) Route(r *http.Request) (RoutingInfo, bool) {
	pol, err := rt.policySelector(r)
	if err != nil {
		rt.logger.Error().Err(err).Msg("Error while selecting pol")
		return noInfo, false
	}

	if _, ok := rt.directors[pol]; !ok {
		rt.logger.
			Error().
			Str("policy", pol).
			Msg("policy is not configured")
		return noInfo, false
	}

	method := ""
	// find matching director
	for _, rtype := range config.RouteTypes {
		var handler func(string, url.URL) bool
		switch rtype {
		case config.QueryRoute:
			handler = queryRouteMatcher
		case config.RegexRoute:
			handler = rt.regexRouteMatcher
		case config.PrefixRoute:
			fallthrough
		default:
			handler = prefixRouteMatcher
		}
		if rt.directors[pol][rtype][r.Method] != nil {
			// use specific method
			method = r.Method
		}

		for _, ri := range rt.directors[pol][rtype][method] {
			if handler(ri.endpoint, *r.URL) {
				rt.logger.Debug().
					Str("policy", pol).
					Str("method", r.Method).
					Str("prefix", ri.endpoint).
					Str("path", r.URL.Path).
					Str("routeType", string(rtype)).
					Msg("director found")

				return ri, true
			}
		}
	}

	// override default director with root. If any
	if ri := rt.directors[pol][config.PrefixRoute][method][0]; ri.endpoint == "/" { // try specific method
		return ri, true
	} else if ri := rt.directors[pol][config.PrefixRoute][""][0]; ri.endpoint == "/" { // fallback to unspecific method
		return ri, true
	}

	rt.logger.
		Warn().
		Str("policy", pol).
		Str("path", r.URL.Path).
		Msg("no director found")
	return noInfo, false
}

func (rt Router) regexRouteMatcher(pattern string, target url.URL) bool {
	matched, err := regexp.MatchString(pattern, target.String())
	if err != nil {
		rt.logger.Warn().Err(err).Str("pattern", pattern).Msg("regex with pattern failed")
	}
	return matched
}

func prefixRouteMatcher(prefix string, target url.URL) bool {
	return strings.HasPrefix(target.Path, prefix) && prefix != "/"
}

func singleJoiningSlash(a, b string) string {
	aslash := strings.HasSuffix(a, "/")
	bslash := strings.HasPrefix(b, "/")
	switch {
	case aslash && bslash:
		return a + b[1:]
	case !aslash && !bslash:
		return a + "/" + b
	}
	return a + b
}

func queryRouteMatcher(endpoint string, target url.URL) bool {
	u, _ := url.Parse(endpoint)
	if !strings.HasPrefix(target.Path, u.Path) || endpoint == "/" {
		return false
	}
	q := u.Query()
	if len(q) == 0 {
		return false
	}
	tq := target.Query()
	for k := range q {
		if q.Get(k) != tq.Get(k) {
			return false
		}
	}
	return true
}

// SetRoutingInfo puts the routing info in the context.
func SetRoutingInfo(parent context.Context, ri RoutingInfo) context.Context {
	return context.WithValue(parent, routingInfoCtxKey{}, ri)
}

// ContextRoutingInfo gets the routing information from the context.
func ContextRoutingInfo(ctx context.Context) RoutingInfo {
	val := ctx.Value(routingInfoCtxKey{})
	return val.(RoutingInfo)
}
