package router

import (
	"net/http"
	"net/url"
	"sort"
	"strings"
	"sync"

	"github.com/go-chi/chi/v5"

	"github.com/pingcap/agent-git-service/internal/githttp"
	"github.com/pingcap/agent-git-service/internal/graphql"
	srvmiddleware "github.com/pingcap/agent-git-service/internal/middleware"
	"github.com/pingcap/agent-git-service/internal/oauth"
	"github.com/pingcap/agent-git-service/internal/rest"
	routecontract "github.com/pingcap/agent-git-service/repositoryroutecontract"
)

// RegisteredRoute is one method-pattern pair owned by the real AGS router.
// Split-edge activation may select from this inventory but must not invent a
// second route registration table.
type RegisteredRoute struct {
	Method         string
	Pattern        string
	Operation      routecontract.Operation
	Authentication routecontract.AuthenticationScope
	Capability     string
}

type RegisteredRouteMatch struct {
	RegisteredRoute
	Repository string
}

var (
	registeredRoutesOnce  sync.Once
	registeredRoutesMux   chi.Router
	registeredRoutesList  []RegisteredRoute
	registeredRoutesByKey map[string]RegisteredRoute
)

// RegisteredRoutes returns a stable copy of the method-pattern inventory
// produced by RegisterRoutes itself.
func RegisteredRoutes() []RegisteredRoute {
	loadRegisteredRoutes()
	return append([]RegisteredRoute(nil), registeredRoutesList...)
}

// MatchRegisteredRoute resolves one concrete request through the registration
// tree and returns the registration-owned operation/auth metadata plus any
// repository selected by chi parameters. It never classifies an unmatched
// prefix as a capability.
func MatchRegisteredRoute(request *http.Request) (RegisteredRouteMatch, bool) {
	if request == nil || request.URL == nil {
		return RegisteredRouteMatch{}, false
	}
	loadRegisteredRoutes()
	routePath, ok := registeredRequestRoutePath(request.URL)
	if !ok {
		return RegisteredRouteMatch{}, false
	}
	ctx := chi.NewRouteContext()
	if !registeredRoutesMux.Match(ctx, request.Method, routePath) {
		return RegisteredRouteMatch{}, false
	}
	registered, ok := registeredRoutesByKey[registeredRouteKey(request.Method, ctx.RoutePattern())]
	if !ok {
		return RegisteredRouteMatch{}, false
	}
	match := RegisteredRouteMatch{RegisteredRoute: registered}
	if strings.Contains(registered.Pattern, "{owner}") && strings.Contains(registered.Pattern, "{repo}") {
		git := strings.HasPrefix(registered.Pattern, "/{owner}/{repo}/")
		match.Repository = routeRepository(ctx, git, request.URL.RawPath != "")
	}
	if registered.Operation == routecontract.OperationGitInfoRefs {
		switch strings.TrimSpace(request.URL.Query().Get("service")) {
		case "", "git-upload-pack":
			match.Capability = "repo:clone"
		case "git-receive-pack":
			match.Capability = "repo:write"
		default:
			match.Capability = ""
		}
	}
	if registered.Pattern == "/api/v3/repos/{owner}/{repo}/archive/refs/tags/{tagfile}" &&
		!registeredArchiveTagFile(ctx.URLParam("tagfile"), request.URL.RawPath != "") {
		match.Capability = ""
	}
	return match, true
}

// registeredRequestRoutePath mirrors chi dispatch while rejecting a RawPath
// that could not have represented Path on an inbound HTTP request. A valid
// RawPath must remain encoded so an escaped slash stays inside one route
// parameter rather than becoming a new path segment.
func registeredRequestRoutePath(requestURL *url.URL) (string, bool) {
	if requestURL == nil {
		return "", false
	}
	path := requestURL.Path
	if requestURL.RawPath != "" {
		decoded, err := url.PathUnescape(requestURL.RawPath)
		if err != nil || decoded != path {
			return "", false
		}
		path = requestURL.RawPath
	}
	if path == "" {
		path = "/"
	}
	return path, true
}

func registeredArchiveTagFile(value string, escaped bool) bool {
	if escaped {
		decoded, err := url.PathUnescape(value)
		if err != nil {
			return false
		}
		value = decoded
	}
	for _, suffix := range []string{".tar.gz", ".zip"} {
		if strings.HasSuffix(value, suffix) {
			return strings.TrimSuffix(value, suffix) != ""
		}
	}
	return false
}

func loadRegisteredRoutes() {
	registeredRoutesOnce.Do(func() {
		router := chi.NewRouter()
		RegisterRoutes(router, &rest.Deps{}, &githttp.Handler{}, &graphql.Server{}, &oauth.Handler{}, "")
		registeredRoutesMux = router
		registeredRoutesByKey = make(map[string]RegisteredRoute)
		authenticationScopes := registeredRouteAuthenticationScopes()
		_ = chi.Walk(router, func(method, pattern string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
			method = strings.ToUpper(strings.TrimSpace(method))
			pattern = strings.TrimSpace(pattern)
			key := registeredRouteKey(method, pattern)
			route := RegisteredRoute{
				Method: method, Pattern: pattern,
				Operation:      routecontract.OperationForRegistration(method, pattern),
				Authentication: authenticationScopes[key],
				Capability:     registeredRouteCapability(method, pattern),
			}
			registeredRoutesList = append(registeredRoutesList, route)
			registeredRoutesByKey[key] = route
			return nil
		})
		sort.Slice(registeredRoutesList, func(i, j int) bool {
			if registeredRoutesList[i].Pattern != registeredRoutesList[j].Pattern {
				return registeredRoutesList[i].Pattern < registeredRoutesList[j].Pattern
			}
			return registeredRoutesList[i].Method < registeredRoutesList[j].Method
		})
	})
}

func registeredRouteKey(method, pattern string) string {
	return strings.ToUpper(strings.TrimSpace(method)) + "\x00" + strings.TrimSpace(pattern)
}

func routeRepository(ctx *chi.Context, git, escaped bool) string {
	owner := strings.TrimSpace(ctx.URLParam("owner"))
	repository := strings.TrimSpace(ctx.URLParam("repo"))
	if escaped {
		var err error
		owner, err = url.PathUnescape(owner)
		if err != nil {
			return ""
		}
		repository, err = url.PathUnescape(repository)
		if err != nil {
			return ""
		}
	}
	if git {
		repository = strings.TrimSuffix(repository, ".git")
	}
	if owner == "" || repository == "" || strings.ContainsAny(owner+repository, "/\\") {
		return ""
	}
	return owner + "/" + repository
}

func registeredRouteCapability(method, pattern string) string {
	method = strings.ToUpper(strings.TrimSpace(method))
	pattern = strings.TrimSpace(pattern)
	switch method + " " + pattern {
	case "GET /{owner}/{repo}/info/refs":
		return "repo:clone"
	case "POST /{owner}/{repo}/git-upload-pack":
		return "repo:clone"
	case "POST /{owner}/{repo}/git-receive-pack":
		return "repo:write"
	}
	if !strings.HasPrefix(pattern, "/api/v3/repos/{owner}/{repo}") &&
		!strings.HasPrefix(pattern, "/api/ext/v1/repos/{owner}/{repo}") {
		return ""
	}
	switch method {
	case http.MethodGet, http.MethodHead:
		return "repo:read"
	case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
		return "repo:write"
	default:
		return ""
	}
}

func registeredRouteAuthenticationScopes() map[string]routecontract.AuthenticationScope {
	result := make(map[string]routecontract.AuthenticationScope)
	identityMiddleware := func(next http.Handler) http.Handler { return next }
	deps := &rest.Deps{}
	git := &githttp.Handler{}
	gql := &graphql.Server{}
	oauthHandler := &oauth.Handler{}
	collect := func(scope routecontract.AuthenticationScope, register func(chi.Router)) {
		router := chi.NewRouter()
		register(router)
		_ = chi.Walk(router, func(method, pattern string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
			result[registeredRouteKey(method, pattern)] = scope
			return nil
		})
	}

	collect(routecontract.AuthenticationPublic, func(router chi.Router) {
		registerOAuthRoutes(router, oauthHandler, srvmiddleware.EmbeddedAuthConfig{})
		registerPublicAuthRoutes(router, deps, identityMiddleware)
		registerAgentPublicRoutes(router, deps, identityMiddleware)
		registerAnalyticsRoutes(router, deps, identityMiddleware)
	})
	collect(routecontract.AuthenticationOptional, func(router chi.Router) {
		registerGitHTTPRoutes(router, git, deps, "", srvmiddleware.EmbeddedAuthConfig{})
		registerAPIDiscoveryRoutes(router, deps, identityMiddleware, srvmiddleware.EmbeddedAuthConfig{})
		registerPublicUserLookupRoutes(router, deps, identityMiddleware, srvmiddleware.EmbeddedAuthConfig{})
		registerPublicRepoRoutes(router, deps, identityMiddleware, srvmiddleware.EmbeddedAuthConfig{})
	})
	collect(routecontract.AuthenticationAuthenticated, func(router chi.Router) {
		registerAuthenticatedRoutes(router, deps, gql, identityMiddleware, srvmiddleware.EmbeddedAuthConfig{})
	})

	// These routes share registration functions with a broader scope but have
	// their own real middleware contract.
	result[registeredRouteKey(http.MethodGet, "/avatars/{login}")] = routecontract.AuthenticationPublic
	for _, key := range []string{
		registeredRouteKey(http.MethodGet, "/login/device"),
		registeredRouteKey(http.MethodPost, "/login/device"),
		registeredRouteKey(http.MethodPost, extensionAPIPrefix+"/oauth/device/approve"),
		registeredRouteKey(http.MethodPost, extensionAPIPrefix+"/oauth/device/reject"),
	} {
		result[key] = routecontract.AuthenticationAuthenticated
	}
	return result
}
