package router

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strings"

	agsserver "github.com/pingcap/agent-git-service/server"

	repositoryroute "github.com/pingcap/agent-git-service/repositoryroutecontract"
	"github.com/tidbcloud/AgentTeam/internal/agsbridge/workspaceurl"
	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/actionsrunner"
	"github.com/tidbcloud/AgentTeam/internal/api/automation"
	"github.com/tidbcloud/AgentTeam/internal/api/kb"
	"github.com/tidbcloud/AgentTeam/internal/api/knowledgeqa"
	"github.com/tidbcloud/AgentTeam/internal/api/memory"
	"github.com/tidbcloud/AgentTeam/internal/api/meshxapi"
	"github.com/tidbcloud/AgentTeam/internal/api/providertokens"
	"github.com/tidbcloud/AgentTeam/internal/api/repocache"
	"github.com/tidbcloud/AgentTeam/internal/api/repotokens"
	"github.com/tidbcloud/AgentTeam/internal/api/roles"
	"github.com/tidbcloud/AgentTeam/internal/api/skills"
	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	kbservice "github.com/tidbcloud/AgentTeam/internal/service/kb"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
	workspaceroute "github.com/tidbcloud/AgentTeam/internal/workspace/router"
)

type RegisterOption func(*registerOptions)

type registerOptions struct {
	repositoryStore *reposstore.Store
	kbStorage       knowledgeBaseStorageResolver
	publicRepoCache *repocache.Handler
	knowledgeQA     *knowledgeqa.Handler
	roleOperations  http.Handler
}

type workspaceAGSAuthority struct {
	groupID      string
	workspaces   workspaceAGSWorkspaceChecker
	resolver     workspaceAGSWorkspaceResolver
	repositories workspaceAGSRepositoryChecker
	next         http.Handler
}

type workspaceAGSRouteAuthority struct {
	workspaceID string
	groupID     string
}

type workspaceAGSWorkspaceChecker interface {
	IsWorkspaceRoutableToGroup(context.Context, string, string) (bool, error)
}

type workspaceAGSWorkspaceResolver interface {
	ResolveWorkspaceRouteTarget(context.Context, string) (workspace.WorkspaceRouteTarget, error)
}

type workspaceAGSRepositoryChecker interface {
	IsBoundToWorkspace(context.Context, string, string) (bool, error)
}

// workspaceAGSRepositoryMutationChecker is implemented by the product-aware
// authority wired at the canonical workspace AGS seam. The seam calls it only
// for repository write capabilities; reads and unmigrated bindings keep their
// existing AGS path.
type workspaceAGSRepositoryMutationChecker interface {
	AllowLegacyRepositoryMutation(context.Context, string, string) (bool, error)
}

func WithPublicRepoCache(handler *repocache.Handler) RegisterOption {
	return func(options *registerOptions) { options.publicRepoCache = handler }
}

func WithKnowledgeQA(handler *knowledgeqa.Handler) RegisterOption {
	return func(options *registerOptions) {
		options.knowledgeQA = handler
	}
}

func WithRoleOperations(handler http.Handler) RegisterOption {
	return func(options *registerOptions) {
		options.roleOperations = handler
	}
}

type knowledgeBaseStorageResolver interface {
	GetKnowledgeBase(ctx context.Context, workspaceID, subject, id string) (storekb.KnowledgeBase, error)
}

func WithRepositoryNameAvailability(store *reposstore.Store, kbStorage knowledgeBaseStorageResolver) RegisterOption {
	return func(options *registerOptions) {
		options.repositoryStore = store
		options.kbStorage = kbStorage
	}
}

func Register(cfg config.ResourceRuntimeConfig, agsHandler http.Handler, kbHandler *kb.KBHandler, automationsHandler *automation.AutomationsHandler, repositoryAccessTokensHandler *repotokens.Handler, personalAccessTokensHandler *providertokens.Handler, actionsRunnerHandler *actionsrunner.Handler, memoryHandler *memory.MemoryHandler, rolesHandler *roles.RolesHandler, skillsHandler *skills.SkillsHandler, opts ...RegisterOption) http.Handler {
	options := registerOptions{}
	for _, opt := range opts {
		if opt != nil {
			opt(&options)
		}
	}
	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", writeOK)
	mux.HandleFunc("GET /readyz", writeOK)
	repocache.RegisterRoutes(mux, options.publicRepoCache)

	authMw := trusted.Middleware(trusted.Options{Enabled: cfg.Auth.Enabled, Header: cfg.Auth.Header, AllowMock: cfg.Auth.AllowMock})
	mcpHandler := meshxapi.NewMCPHandler()
	kb.RegisterRoutes(mux, kbHandler, authMw)
	knowledgeqa.RegisterRuntimeRoutes(mux, options.knowledgeQA, authMw)
	memory.RegisterRoutes(mux, memoryHandler, authMw)
	// The foundation has no deployable generic Content Store capability. Keep
	// its namespace from falling through to the global AGS compatibility handler.
	mux.Handle("/api/v1/content-store", http.NotFoundHandler())
	mux.Handle("/api/v1/content-store/", http.NotFoundHandler())
	skills.RegisterRoutes(mux, skillsHandler, authMw)
	roles.RegisterRoutes(mux, rolesHandler, authMw)
	roles.RegisterOperationRoutes(mux, options.roleOperations, authMw)
	mux.Handle("GET /api/v1/repository-owners", authMw(meshxapi.HandleAuth(repositoryOwners, repositoryOwnerErrorHandling())))
	mux.Handle("GET /api/v1/repository-names/available", authMw(meshxapi.HandleAuth(repositoryNameAvailability(options.repositoryStore, options.kbStorage), repositoryOwnerErrorHandling())))
	mux.Handle("/api/v1/mcp/", authMw(authenticated(mcpHandler.Placeholder)))
	mux.Handle("/api/v1/mcp", authMw(authenticated(mcpHandler.Placeholder)))
	automation.RegisterRoutes(mux, automationsHandler, authMw)
	repotokens.RegisterRoutes(mux, repositoryAccessTokensHandler, authMw)
	providertokens.RegisterRoutes(mux, personalAccessTokensHandler, authMw)
	if !cfg.Automation.HasRole(config.AutomationRoleRunnerDispatch) {
		actionsRunnerHandler = nil
	}
	actionsrunner.RegisterRoutes(mux, actionsRunnerHandler)

	agsProxy := stripMeshXInternalProxyHeaders(agsHandler)
	protectedAGS := agsCredentialAwareAuth(authMw, agsProxy, cfg.Auth.Header)
	mux.Handle("/api/ext/v1/", protectedAGS)
	mux.Handle("/api/ext/v1", protectedAGS)
	mux.Handle("/api/v3/", protectedAGS)
	mux.Handle("/api/v3", protectedAGS)
	mux.Handle("/api/graphql", protectedAGS)
	mux.Handle("/api/graphql/", protectedAGS)
	mux.Handle("/", globalAGSOrGitSmartHTTP(agsProxy))
	return mux
}

func workspaceAGSSeam(cfg config.ResourceRuntimeConfig, authority workspaceAGSAuthority, fallback http.Handler) http.Handler {
	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		if !workspaceurl.Owns(request) {
			fallback.ServeHTTP(writer, request)
			return
		}
		qualifiedPath, err := workspaceurl.Parse(request)
		if err != nil {
			http.Error(writer, "invalid workspace AGS path", http.StatusBadRequest)
			return
		}
		nativeRequest := request.Clone(request.Context())
		if err := qualifiedPath.Strip(nativeRequest, qualifiedPath.WorkspaceID); err != nil {
			http.Error(writer, "invalid workspace AGS path", http.StatusBadRequest)
			return
		}
		scope, activated := activatedWorkspaceAGSScope(nativeRequest)
		if !activated {
			http.NotFound(writer, request)
			return
		}
		routeAuthority, status, message := resolveWorkspaceAGSAuthority(request, qualifiedPath.WorkspaceID, authority)
		if status != 0 {
			http.Error(writer, message, status)
			return
		}
		nativeRequest.Header.Set(api.HeaderRouteWorkspaceID, routeAuthority.workspaceID)
		nativeRequest.Header.Set(api.HeaderWorkspaceID, routeAuthority.workspaceID)
		nativeRequest.Header.Set(workspaceroute.HeaderResolvedWorkspaceGroupID, routeAuthority.groupID)
		if scope.Repository != "" {
			if authority.repositories == nil {
				http.Error(writer, "workspace repository authority is unavailable", http.StatusServiceUnavailable)
				return
			}
			bound, err := authority.repositories.IsBoundToWorkspace(request.Context(), routeAuthority.workspaceID, scope.Repository)
			if err != nil {
				http.Error(writer, "workspace repository authority is unavailable", http.StatusServiceUnavailable)
				return
			}
			if !bound {
				http.NotFound(writer, request)
				return
			}
			if scope.Capability == repos.RepositorySyncCapabilityWrite {
				mutations, ok := authority.repositories.(workspaceAGSRepositoryMutationChecker)
				if !ok {
					http.Error(writer, "workspace repository mutation authority is unavailable", http.StatusServiceUnavailable)
					return
				}
				allowed, err := mutations.AllowLegacyRepositoryMutation(request.Context(), routeAuthority.workspaceID, scope.Repository)
				if err != nil {
					http.Error(writer, "workspace repository mutation authority is unavailable", http.StatusServiceUnavailable)
					return
				}
				if !allowed {
					http.Error(writer, "workspace repository legacy mutations are disabled", http.StatusConflict)
					return
				}
			}
		}
		publicBase, err := qualifiedPath.PublicBaseURL(cfg.PublicBaseURL)
		if err != nil {
			http.Error(writer, "workspace AGS public base is unavailable", http.StatusServiceUnavailable)
			return
		}
		serveAGS := http.HandlerFunc(func(response http.ResponseWriter, native *http.Request) {
			agsserver.ServeWithPublicURLBase(authority.next, response, native, publicBase)
		})
		if scope.Raw != nil {
			serveWorkspaceRawContent(serveAGS, writer, nativeRequest, *scope.Raw)
			return
		}
		serveAGS.ServeHTTP(writer, nativeRequest)
	})
}

func resolveWorkspaceAGSAuthority(request *http.Request, pathWorkspaceID string, authority workspaceAGSAuthority) (workspaceAGSRouteAuthority, int, string) {
	if authority.resolver != nil {
		target, err := authority.resolver.ResolveWorkspaceRouteTarget(request.Context(), pathWorkspaceID)
		if err != nil {
			status := api.StatusForError(err)
			message := "workspace AGS route authority is unavailable"
			if status < http.StatusInternalServerError {
				message = err.Error()
			}
			return workspaceAGSRouteAuthority{}, status, message
		}
		resolved := workspaceAGSRouteAuthority{
			workspaceID: strings.TrimSpace(target.WorkspaceID),
			groupID:     strings.TrimSpace(target.WorkspaceGroupID),
		}
		if resolved.workspaceID != pathWorkspaceID || resolved.groupID == "" {
			return workspaceAGSRouteAuthority{}, http.StatusConflict, "workspace AGS route authority does not match the canonical path"
		}
		return resolved, 0, ""
	}

	resolved := workspaceAGSRouteAuthority{
		workspaceID: strings.TrimSpace(request.Header.Get(api.HeaderRouteWorkspaceID)),
		groupID:     strings.TrimSpace(request.Header.Get(workspaceroute.HeaderResolvedWorkspaceGroupID)),
	}
	if resolved.workspaceID == "" || resolved.workspaceID != pathWorkspaceID || authority.groupID == "" || resolved.groupID != authority.groupID || authority.workspaces == nil {
		return workspaceAGSRouteAuthority{}, http.StatusBadRequest, "workspace AGS route authority is required"
	}
	owned, err := authority.workspaces.IsWorkspaceRoutableToGroup(request.Context(), resolved.workspaceID, authority.groupID)
	if err != nil || !owned {
		return workspaceAGSRouteAuthority{}, http.StatusBadRequest, "workspace AGS route authority is required"
	}
	return resolved, 0, ""
}

func activatedWorkspaceAGSScope(request *http.Request) (repositoryroute.Scope, bool) {
	if synthetic, ok := repositoryroute.ClassifySynthetic(request); ok {
		return synthetic, true
	}
	registered, ok := agsserver.MatchRegisteredRoute(request)
	if !ok {
		return repositoryroute.Scope{RegisteredScope: repositoryroute.ScopeUnsupported}, false
	}
	authority, activated := repositoryroute.ActivatedAuthority(registered.Operation)
	if !activated {
		return repositoryroute.Scope{RegisteredScope: repositoryroute.ScopeUnsupported}, false
	}
	scope := repositoryroute.ScopeFromRegistration(registered.Operation, registered.Authentication, authority, registered.Repository, registered.Capability)
	return scope, scope.EdgePolicy != repositoryroute.ScopeUnsupported
}

// WorkspaceAGSSeam is the target-side authority for the canonical outer seam.
// It invokes the real AGS handler only after exact activation, local placement,
// and repository/workspace binding checks have all succeeded.
func WorkspaceAGSSeam(cfg config.ResourceRuntimeConfig, groupID string, workspaces workspaceAGSWorkspaceChecker, repositories workspaceAGSRepositoryChecker, next, fallback http.Handler) http.Handler {
	return workspaceAGSSeam(cfg, workspaceAGSAuthority{
		groupID: strings.TrimSpace(groupID), workspaces: workspaces, repositories: repositories, next: next,
	}, fallback)
}

// StandaloneWorkspaceAGSSeam resolves canonical requests from authoritative
// placement data in all-in-one deployments. It never trusts caller-supplied
// routing headers or assumes that the workspace uses a configured default group.
func StandaloneWorkspaceAGSSeam(cfg config.ResourceRuntimeConfig, workspaces workspaceAGSWorkspaceResolver, repositories workspaceAGSRepositoryChecker, next, fallback http.Handler) http.Handler {
	return workspaceAGSSeam(cfg, workspaceAGSAuthority{
		resolver: workspaces, repositories: repositories, next: next,
	}, fallback)
}

func agsCredentialAwareAuth(authMw func(http.Handler) http.Handler, next http.Handler, trustedIdentityHeader string) http.Handler {
	sessionProtected := authMw(next)
	trustedIdentityHeader = strings.TrimSpace(trustedIdentityHeader)
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if agsserver.IsGlobalRoute(r.Method, r.URL.Path) {
			next.ServeHTTP(w, r)
			return
		}
		credential := agsserver.RepositoryCredential(r)
		_, _, repositoryDataPlane := agsserver.RepositoryAuthorizationTarget(r)
		if credential != "" && (repositoryDataPlane ||
			trustedIdentityHeader == "" || strings.TrimSpace(r.Header.Get(trustedIdentityHeader)) == "") {
			next.ServeHTTP(w, r)
			return
		}
		sessionProtected.ServeHTTP(w, r)
	})
}

func repositoryOwners(request meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	owners, err := repos.ResolveRepositoryOwners(request.WorkspaceID(), request.Subject())
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(owners), nil
}

func repositoryNameAvailability(store *reposstore.Store, kbStorage knowledgeBaseStorageResolver) func(meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	return func(request meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
		query := request.Request.URL.Query()
		input := repos.RepositoryNameAvailabilityRequest{
			WorkspaceID:       request.WorkspaceID(),
			Subject:           request.Subject(),
			ResourceType:      query.Get("resourceType"),
			Name:              query.Get("name"),
			RepoNameOverride:  query.Get("repoNameOverride"),
			StorageScope:      query.Get("storageScope"),
			StorageOwnerLogin: query.Get("storageOwnerLogin"),
		}
		if strings.TrimSpace(input.ResourceType) == reposstore.ResourceTypeKBSource {
			if kbStorage == nil {
				return meshxapi.Response{}, meshxapi.NewHTTPError(http.StatusServiceUnavailable, "knowledge base storage resolver is not configured")
			}
			kbID := strings.TrimSpace(query.Get("knowledgeBaseId"))
			if kbID == "" {
				return meshxapi.Response{}, meshxapi.NewHTTPError(http.StatusBadRequest, "knowledgeBaseId is required for kb_source availability")
			}
			base, err := kbStorage.GetKnowledgeBase(request.Context(), request.WorkspaceID(), request.Subject(), kbID)
			if err != nil {
				return meshxapi.Response{}, err
			}
			input.StorageScope = base.StorageScope
			input.StorageOwnerLogin = base.StorageOwnerLogin
		}
		availability, err := repos.CheckRepositoryNameAvailability(request.Context(), store, input)
		if err != nil {
			return meshxapi.Response{}, err
		}
		return meshxapi.OK(availability), nil
	}
}

func repositoryOwnerErrorHandling() meshxapi.HandlerOption {
	return meshxapi.WithErrorClassifier(func(err error) (meshxapi.ErrorResponse, bool) {
		if errors.Is(err, kbservice.ErrUnauthorized) || errors.Is(err, kbservice.ErrInvalidArgument) || errors.Is(err, kbservice.ErrForbidden) || errors.Is(err, kbservice.ErrNotFound) || errors.Is(err, kbservice.ErrConflict) {
			return meshxapi.ErrorResponse{Status: kbservice.HTTPStatus(err), Message: err.Error(), Code: kbservice.Code(err), Details: kbservice.Details(err)}, true
		}
		if errors.Is(err, repos.ErrUnauthorized) || errors.Is(err, repos.ErrInvalidArgument) || errors.Is(err, repos.ErrForbidden) || errors.Is(err, repos.ErrNotFound) || errors.Is(err, repos.ErrConflict) {
			return meshxapi.ErrorResponse{Status: repos.HTTPStatus(err), Message: err.Error(), Code: repos.Code(err), Details: repos.Details(err)}, true
		}
		return meshxapi.ErrorResponse{}, false
	})
}

func stripMeshXInternalProxyHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		r.Header.Del("X-MeshX-Acting-Principal")
		next.ServeHTTP(w, r)
	})
}

func authenticated(handler func(meshxapi.AuthenticatedRequest) (meshxapi.Response, error)) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		identity, ok := trusted.IdentityFromContext(r.Context())
		if !ok {
			meshxapi.WriteError(w, meshxapi.NewHTTPError(http.StatusUnauthorized, "Missing authenticated identity"))
			return
		}
		response, err := handler(meshxapi.AuthenticatedRequest{Request: r, Identity: identity})
		if err != nil {
			meshxapi.WriteError(w, err)
			return
		}
		meshxapi.WriteResponse(w, response)
	})
}

func writeOK(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

func gitSmartHTTPOnly(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		path := r.URL.Path
		if strings.Contains(path, ".git/") && (strings.HasSuffix(path, "/info/refs") || strings.HasSuffix(path, "/git-upload-pack") || strings.HasSuffix(path, "/git-receive-pack")) {
			next.ServeHTTP(w, r)
			return
		}
		http.NotFound(w, r)
	})
}

func globalAGSOrGitSmartHTTP(next http.Handler) http.Handler {
	git := gitSmartHTTPOnly(next)
	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		if request != nil && request.URL != nil && agsserver.IsGlobalRoute(request.Method, request.URL.Path) {
			next.ServeHTTP(writer, request)
			return
		}
		git.ServeHTTP(writer, request)
	})
}
