// Package router define the struct Router for routing different service requests.
package router

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

	"github.com/pingcap/log"
	"github.com/tidbcloud/clusterproxy/route"
	"go.uber.org/zap"
)

var registeredRoutes []*route.Route

type middleware = func(http.Handler) http.Handler

// Router routes request to different handlers.
type Router struct {
	routes      []*route.Route
	middlewares []middleware
	resolvers   []route.Resolver
	ctx         *route.Context
}

// New returns a new router with the given context and middlewares.
func New(ctx *route.Context, middlewares ...middleware) *Router {
	r := &Router{
		routes:      registeredRoutes,
		middlewares: middlewares,
		ctx:         ctx,
	}
	for _, route := range r.routes {
		regexp.MustCompile(route.Schema.PathRegex)
	}
	return r
}

// RegisterResolver registers a resolver to the router.
func (r *Router) RegisterResolver(res route.Resolver) {
	r.resolvers = append(r.resolvers, res)
}

// Bind binds the mux with the routes registered.
func (r *Router) Bind(mux *http.ServeMux) error {
	scheme, err := parseRoutes(r.routes)
	if err != nil {
		return fmt.Errorf("error parsing the routes: %v", err)
	}
	for _, res := range r.resolvers {
		mux.Handle(
			res.Prefix(),
			r.withMiddlewares(r.withResolver(res, scheme)),
		)
	}
	return nil
}

func (r *Router) withResolver(res route.Resolver, scheme map[string]*route.Route) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		cl, serviceRoute, err := res.Resolve(req)
		if err != nil {
			// Log down the internal error but not surface it to the end user.
			log.Warn("Unable to locate the cluster from the path", zap.String("path", req.URL.Path), zap.Error(err))
			http.Error(w, fmt.Sprintf("unable to locate the cluster from the path %s", req.URL.Path), http.StatusBadRequest)
			return
		}
		for path, rt := range scheme {
			if strings.HasPrefix(serviceRoute, path) {
				req.URL.Path = strings.TrimPrefix(req.URL.Path, path)
				req.URL.Path = strings.TrimPrefix(req.URL.Path, "/")
				rt.WithValidation(r.ctx, cl).ServeHTTP(w, req)
				return
			}
		}
		http.Error(w, fmt.Sprintf("Service route %s not found", serviceRoute), http.StatusNotFound)
	})
}

// WithMiddlewares wraps the given handler with the middlewares.
func (r *Router) withMiddlewares(h http.Handler) http.Handler {
	for _, m := range r.middlewares {
		h = m(h)
	}
	return h
}

// RegisterRoute adds the given route to the registered routes.
func RegisterRoute(r *route.Route) {
	if r.Name == "" {
		panic("The API must define a name")
	}
	if r.Version == "" {
		panic(fmt.Sprintf("The API %s must define a version", r.Name))
	}
	registeredRoutes = append(registeredRoutes, r)
}

// CloneRegisteredRoutes returns a copy of the registered routes.
func CloneRegisteredRoutes() []*route.Route {
	var result []*route.Route
	result = append(result, registeredRoutes...)
	return result
}

func parseRoutes(routes []*route.Route) (map[string]*route.Route, error) {
	known := make(map[string]bool)
	for _, r := range routes {
		if known[r.Name] {
			return nil, fmt.Errorf("more than one route named %s", r.Name)
		}
		known[r.Name] = true
	}

	routeMap := make(map[string]*route.Route)

	for _, r := range routes {
		for _, v := range route.GeneratedVersions(r.Version) {
			vp := joinWithLastSlash(v.AsPrefix(), r.Path)
			if rt, found := routeMap[vp]; found {
				return nil, fmt.Errorf("versioned path %s already registered by %s", vp, rt.Name)
			}
			routeMap[vp] = r
		}
	}
	return routeMap, nil
}

func joinWithLastSlash(a, b string) string {
	if b == "" {
		return a
	}
	hasLastSlash := string(b[len(b)-1]) == "/"
	if hasLastSlash {
		return path.Join(a, b) + "/"
	}
	return path.Join(a, b)
}
