// Package forwarder contains AgentTeam's authenticated HTTP reverse proxy routes for configured upstream APIs.
package forwarder

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"
	"unicode"

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

	repositoryroute "github.com/pingcap/agent-git-service/repositoryroutecontract"
	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/passwords"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
	"github.com/tidbcloud/AgentTeam/internal/workspace/owner"
)

const (
	defaultProxyTimeout = 30 * time.Second
	trustedJWTHeader    = config.DefaultTrustedJWTHeader
	basicAuthChallenge  = `Basic realm="AgentTeam"`
)

// AuthStore is the browser-app auth middleware port.
type AuthStore interface {
	api.SessionAuthenticator
}

type passwordAuthStore interface {
	FindUserByLoginIdentifier(ctx context.Context, identifier string) (identity.User, error)
}

type usernameAuthStore interface {
	FindUserByName(ctx context.Context, name string) (identity.User, error)
}

// Dependencies wires API forwarder routes.
type Dependencies struct {
	AuthStore                  AuthStore
	WorkspaceStore             api.WorkspaceResolver
	BaseURL                    string
	RequestTimeout             time.Duration
	HTTPClient                 *http.Client
	EmailVerificationRequired  bool
	PasswordAuthDisabled       bool
	Logger                     *slog.Logger
	Now                        func() time.Time
	TrustedJWTAudience         string
	AgentThreadSessions        AgentThreadSessionChannelStore
	AgentStore                 OwnedAgentStore
	MeshXEnabled               bool
	BillingEnabled             bool
	PlanStore                  WorkspaceResourceLimitStore
	ManagedWorkspaces          owner.ManagedWorkspaceSet
	EnforceRouteWorkspaceOwner bool
}

// AgentThreadSessionChannelStore owns hidden agent-thread/session carrier channels used by generic channel/thread APIs.
type AgentThreadSessionChannelStore interface {
	EnsureAgentThreadSessionChannelForUser(ctx context.Context, userID string, workspaceID string) (message.ChannelSummary, error)
}

type OwnedAgentStore interface {
	ListOwnedAgentsForWorkspace(ctx context.Context, userID string, workspaceID string) ([]agent.AppAgent, error)
}

type WorkspaceResourceLimitStore interface {
	LoadWorkspaceResourceLimit(ctx context.Context, workspaceID string, ownerUserID string, metric plan.MetricKey) (plan.WorkspaceResourceLimit, error)
}

// RegisterRoutes mounts configured upstream API routes.
func RegisterRoutes(router *gin.Engine, dependencies Dependencies) {
	if router == nil {
		return
	}
	handler, err := NewHandler(dependencies)
	if err != nil {
		if dependencies.Logger != nil {
			dependencies.Logger.Warn("register disabled API forwarder routes", slog.Any("error", err))
		}
		handler = &Handler{
			agentThreadSessions: dependencies.AgentThreadSessions,
			workspaceStore:      dependencies.WorkspaceStore,
			logger:              dependencies.Logger,
			billingEnabled:      dependencies.BillingEnabled,
			planStore:           dependencies.PlanStore,
		}
	}
	authenticated := []gin.HandlerFunc{
		api.RequireAuthenticatedUser(dependencies.AuthStore),
		api.RequireVerifiedIdentity(dependencies.EmailVerificationRequired),
	}
	gitAuthenticated := []gin.HandlerFunc{
		requireForwarderAuthenticatedUser(dependencies.AuthStore, dependencies.PasswordAuthDisabled),
		api.RequireVerifiedIdentity(dependencies.EmailVerificationRequired),
	}
	if dependencies.EnforceRouteWorkspaceOwner {
		gitAuthenticated = append(gitAuthenticated,
			api.RequireCurrentWorkspace(api.NewOwnedWorkspaceResolver(dependencies.WorkspaceStore, dependencies.ManagedWorkspaces)),
		)
	}
	workspaceQualifiedV1 := router.Group("/api/v1/workspaces/:workspaceId", authenticated...)
	workspaceQualifiedV1.Use(api.RequireRouteWorkspace(api.NewOwnedWorkspaceResolver(dependencies.WorkspaceStore, dependencies.ManagedWorkspaces)))
	meshXProxy := forwardWorkspaceQualifiedMeshX(handler)
	workspaceQualifiedV1.Any("/kb", meshXProxy)
	workspaceQualifiedV1.Any("/kb/*forwardPath", meshXProxy)
	workspaceQualifiedV1.Any("/kb-publication-reviews", meshXProxy)
	workspaceQualifiedV1.Any("/kb-publication-reviews/*forwardPath", meshXProxy)
	workspaceQualifiedV1.Any("/memory", meshXProxy)
	workspaceQualifiedV1.Any("/memory/*forwardPath", meshXProxy)
	workspaceQualifiedV1.Any("/skills", meshXProxy)
	workspaceQualifiedV1.Any("/skills/*forwardPath", meshXProxy)
	rolesV1 := workspaceQualifiedV1.Group("", requireMeshXEnabled(dependencies.MeshXEnabled))
	rolesV1.Any("/roles", meshXProxy)
	rolesV1.Any("/roles:from-agent", meshXProxy)
	rolesV1.Any("/roles/*forwardPath", meshXProxy)
	workspaceQualifiedV1.Any("/repository-owners", meshXProxy)
	workspaceQualifiedV1.Any("/repository-names/available", meshXProxy)
	workspaceQualifiedV1.Any("/mcp", meshXProxy)
	workspaceQualifiedV1.Any("/mcp/*forwardPath", meshXProxy)
	repositoryTokenV1 := workspaceQualifiedV1.Group("", requireMeshXEnabled(dependencies.MeshXEnabled), requireWorkspaceManagePermission())
	repositoryTokenV1.Any("/repository-access-tokens", meshXProxy)
	repositoryTokenV1.Any("/repository-access-tokens/*forwardPath", meshXProxy)
	repositoryTokenV1.Any("/internal/repository-access-tokens/*forwardPath", meshXProxy)
	personalTokenV1 := workspaceQualifiedV1.Group("", requireMeshXEnabled(dependencies.MeshXEnabled))
	personalTokenV1.Any("/personal-access-tokens", meshXProxy)
	personalTokenV1.Any("/personal-access-tokens/*forwardPath", meshXProxy)
	workspaceQualifiedV1.Any("/repositories", meshXProxy)
	workspaceQualifiedV1.Any("/repositories/*forwardPath", meshXProxy)
	workspaceQualifiedV1.GET("/agent-thread/session-channel", handler.ensureAgentThreadSessionChannel)
	automationV1 := workspaceQualifiedV1.Group("", requireMeshXEnabled(dependencies.MeshXEnabled))
	automationV1.Any("/automations", meshXProxy)
	automationV1.Any("/automations/*forwardPath", meshXProxy)

	// The unqualified native AGS endpoints belong only to the all-in-one profile.
	// Split Workspace profiles expose reviewed operation families through their
	// workspace-qualified owners instead of retaining a path-prefix
	// compatibility router.
	if !dependencies.EnforceRouteWorkspaceOwner {
		agsAPI := forwardAGSAPI(handler, gitAuthenticated, dependencies.AuthStore, dependencies.PasswordAuthDisabled)
		router.Any("/api/ext/v1/*forwardPath", agsAPI)
		router.Any("/api/v3/*forwardPath", agsAPI)
		router.Any("/api/graphql", agsAPI)
		globalAGS := forwardAGSGlobal(handler)
		router.Any("/login/*forwardPath", globalAGS)
		router.Any("/auth/connected/*forwardPath", globalAGS)
		router.GET("/avatars/:login", globalAGS)
		// AGS contents responses expose repository raw URLs as
		// /{owner}/{repo}/raw/{ref}/{path}. The all-in-one profile retains this
		// compatibility path; split profiles fail closed until the product
		// workspace locator is activated.
		router.GET("/:owner/:repo/raw/*rawPath", forwardAGSRaw(handler, gitAuthenticated))
	}
}

func forwardWorkspaceQualifiedMeshX(handler *Handler) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		workspaceID := strings.TrimSpace(ctx.Param("workspaceId"))
		if !stripWorkspaceQualifiedPrefix(ctx.Request, workspaceID) {
			api.Error(ctx, http.StatusBadRequest, "Invalid canonical workspace path")
			ctx.Abort()
			return
		}
		handler.Proxy(ctx)
	}
}

func stripWorkspaceQualifiedPrefix(request *http.Request, workspaceID string) bool {
	if request == nil || request.URL == nil || !api.IsValidRouteWorkspaceID(workspaceID) {
		return false
	}
	prefix := "/api/v1/workspaces/" + workspaceID
	if !strings.HasPrefix(request.URL.Path, prefix+"/") {
		return false
	}
	request.URL.Path = "/api/v1" + strings.TrimPrefix(request.URL.Path, prefix)
	if request.URL.RawPath == "" {
		return true
	}
	if !strings.HasPrefix(request.URL.RawPath, prefix+"/") {
		return false
	}
	request.URL.RawPath = "/api/v1" + strings.TrimPrefix(request.URL.RawPath, prefix)
	return true
}

func requireForwarderRouteWorkspaceOwner(dependencies Dependencies) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if !ensureForwarderRouteWorkspaceOwner(ctx, dependencies) {
			return
		}
		ctx.Next()
	}
}

func forwardAGSGlobal(handler *Handler) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if !agsserver.IsGlobalRoute(ctx.Request.Method, ctx.Request.URL.Path) {
			ctx.AbortWithStatus(http.StatusNotFound)
			return
		}
		handler.ProxyDataPlane(ctx)
	}
}

func ensureForwarderRouteWorkspaceOwner(ctx *gin.Context, dependencies Dependencies) bool {
	if !dependencies.EnforceRouteWorkspaceOwner {
		return true
	}
	workspaceID := strings.TrimSpace(ctx.GetHeader(api.HeaderRouteWorkspaceID))
	if workspaceID == "" {
		api.Error(ctx, http.StatusBadRequest, "Route workspace identity is required")
		ctx.Abort()
		return false
	}
	if !api.EnsureWorkspaceOwner(ctx, dependencies.ManagedWorkspaces, workspaceID) {
		ctx.Abort()
		return false
	}
	if current, ok := api.WorkspaceID(ctx); ok && current != workspaceID {
		api.Error(ctx, http.StatusBadRequest, "Route workspace identity conflicts with current workspace")
		ctx.Abort()
		return false
	}
	return true
}

// SmartHTTPNoRouteHandler returns a NoRoute handler that forwards AGS smart HTTP
// `/{owner}/{repo}.git/...` paths and delegates all other paths to fallback.
func SmartHTTPNoRouteHandler(dependencies Dependencies, fallback gin.HandlerFunc) gin.HandlerFunc {
	handler, err := NewHandler(dependencies)
	if err != nil {
		return fallback
	}
	return func(ctx *gin.Context) {
		if isSmartHTTPGitPath(ctx.Request.URL.Path) {
			if !ensureForwarderRouteWorkspaceOwner(ctx, dependencies) {
				return
			}
			handler.ProxyDataPlane(ctx)
			return
		}
		fallback(ctx)
	}
}

func forwardAGSAPI(handler *Handler, authenticated []gin.HandlerFunc, auth AuthStore, passwordAuthDisabled bool) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if isAGSRepositoryRESTDataPlaneRequest(ctx.Request) || useAGSDataPlaneCredential(ctx, auth, passwordAuthDisabled) {
			handler.ProxyDataPlane(ctx)
			return
		}
		for _, middleware := range authenticated {
			middleware(ctx)
			if ctx.IsAborted() {
				return
			}
		}
		handler.Proxy(ctx)
	}
}

func useAGSDataPlaneCredential(ctx *gin.Context, auth AuthStore, passwordAuthDisabled bool) bool {
	raw := strings.TrimSpace(ctx.GetHeader("Authorization"))
	fields := strings.Fields(raw)
	if len(fields) != 2 || strings.TrimSpace(fields[1]) == "" {
		return false
	}
	switch strings.ToLower(fields[0]) {
	case "token":
		return true
	case "bearer":
		if strings.HasPrefix(fields[1], "ags_rat_") {
			return true
		}
		if auth == nil {
			return true
		}
		authenticated, err := auth.AuthenticateSession(ctx.Request.Context(), fields[1])
		if err != nil {
			return true
		}
		api.SetAuthenticatedUser(ctx, authenticated)
		return false
	case "basic":
		if passwordAuthDisabled || auth == nil {
			return true
		}
		username, password, ok := basicCredentials(raw)
		if !ok {
			return true
		}
		authenticated, err := authenticateBasicPassword(ctx.Request.Context(), auth, username, password)
		if err != nil {
			return true
		}
		api.SetAuthenticatedUser(ctx, authenticated)
		return false
	default:
		return false
	}
}

func forwardAGSRaw(handler *Handler, authenticated []gin.HandlerFunc) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if hasAGSRepositoryCredential(ctx.GetHeader("Authorization")) {
			handler.RawRepositoryContentDataPlane(ctx)
			return
		}
		for _, middleware := range authenticated {
			middleware(ctx)
			if ctx.IsAborted() {
				return
			}
		}
		handler.RawRepositoryContent(ctx)
	}
}

func requireMeshXEnabled(enabled bool) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if !enabled {
			api.ErrorWithCode(ctx, http.StatusForbidden, "MeshX is required", "meshx_gate_required")
			ctx.Abort()
			return
		}
		ctx.Next()
	}
}

func requireWorkspaceManagePermission() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		workspaceSummary, ok := api.CurrentWorkspaceFromContext(ctx)
		if !ok {
			api.ErrorWithCode(ctx, http.StatusForbidden, "Permission denied", "permission_denied")
			ctx.Abort()
			return
		}
		if err := access.AuthorizeWorkspaceRole(domain.WorkspaceRole(workspaceSummary.WorkspaceRole), access.HumanAction{Resource: access.ResourceWorkspace, Action: access.ActionManageRuntime}); err != nil {
			api.ErrorWithCode(ctx, http.StatusForbidden, "Permission denied", "permission_denied")
			ctx.Abort()
			return
		}
		ctx.Next()
	}
}

func requireForwarderAuthenticatedUser(auth AuthStore, passwordAuthDisabled bool) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if _, ok := api.AuthenticatedUserFromContext(ctx); ok {
			ctx.Next()
			return
		}
		if auth == nil {
			api.Error(ctx, http.StatusUnauthorized, "Invalid access token")
			ctx.Abort()
			return
		}
		if token, ok := api.BearerToken(ctx); ok {
			authenticated, err := auth.AuthenticateSession(ctx.Request.Context(), token)
			if err != nil {
				api.Error(ctx, http.StatusUnauthorized, "Invalid access token")
				ctx.Abort()
				return
			}
			api.SetAuthenticatedUser(ctx, authenticated)
			ctx.Next()
			return
		}
		username, password, ok := basicCredentials(ctx.GetHeader("Authorization"))
		if !ok {
			writeBasicAuthError(ctx, "Missing AgentTeam credentials")
			ctx.Abort()
			return
		}
		if passwordAuthDisabled {
			api.Error(ctx, http.StatusServiceUnavailable, "Password auth is disabled")
			ctx.Abort()
			return
		}
		authenticated, err := authenticateBasicPassword(ctx.Request.Context(), auth, username, password)
		if err != nil {
			writeBasicAuthError(ctx, "Invalid credentials")
			ctx.Abort()
			return
		}
		api.SetAuthenticatedUser(ctx, authenticated)
		ctx.Next()
	}
}

func writeBasicAuthError(ctx *gin.Context, message string) {
	ctx.Header("WWW-Authenticate", basicAuthChallenge)
	api.Error(ctx, http.StatusUnauthorized, message)
}

func authenticateBasicPassword(ctx context.Context, auth AuthStore, username string, password string) (identity.AuthenticatedUser, error) {
	user, err := findBasicAuthUser(ctx, auth, username)
	if err != nil {
		return identity.AuthenticatedUser{}, err
	}
	passwordOK, err := passwords.Verify(password, user.PasswordHash)
	if err != nil {
		return identity.AuthenticatedUser{}, err
	}
	if !passwordOK {
		return identity.AuthenticatedUser{}, identity.ErrUnauthorized
	}
	user.PasswordHash = ""
	return identity.AuthenticatedUser{
		User: user,
		Session: model.UserSession{
			UserID: user.ID,
		},
	}, nil
}

func findBasicAuthUser(ctx context.Context, auth AuthStore, username string) (identity.User, error) {
	loginAuth, hasLoginAuth := auth.(passwordAuthStore)
	nameAuth, hasNameAuth := auth.(usernameAuthStore)
	if !hasLoginAuth && !hasNameAuth {
		return identity.User{}, identity.ErrUnauthorized
	}

	var lastErr error
	for _, candidate := range basicUsernameCandidates(username) {
		if hasLoginAuth {
			user, err := loginAuth.FindUserByLoginIdentifier(ctx, candidate)
			if err == nil {
				return user, nil
			}
			lastErr = err
		}
		if hasNameAuth && isAgentTeamUsernameCandidate(candidate) {
			user, err := nameAuth.FindUserByName(ctx, candidate)
			if err == nil {
				return user, nil
			}
			lastErr = err
		}
	}
	if lastErr != nil {
		return identity.User{}, lastErr
	}
	return identity.User{}, identity.ErrUnauthorized
}

func basicUsernameCandidates(username string) []string {
	username = strings.TrimSpace(username)
	if username == "" {
		return nil
	}
	candidates := []string{username}
	if unescaped, err := url.PathUnescape(username); err == nil {
		unescaped = strings.TrimSpace(unescaped)
		if unescaped != "" && unescaped != username {
			candidates = []string{unescaped, username}
		}
	}
	return candidates
}

func isAgentTeamUsernameCandidate(username string) bool {
	username = strings.TrimSpace(username)
	return username != "" && !strings.Contains(username, "@")
}

func basicCredentials(raw string) (string, string, bool) {
	parts := strings.Fields(strings.TrimSpace(raw))
	if len(parts) != 2 || !strings.EqualFold(parts[0], "Basic") {
		return "", "", false
	}
	decoded, err := base64.StdEncoding.DecodeString(parts[1])
	if err != nil {
		return "", "", false
	}
	username, password, ok := strings.Cut(string(decoded), ":")
	if !ok {
		return "", "", false
	}
	username = strings.TrimSpace(username)
	password = strings.TrimSpace(password)
	if username == "" || password == "" {
		return "", "", false
	}
	return username, password, true
}

// Handler forwards authenticated AgentTeam requests to the configured upstream without rewriting paths.
type Handler struct {
	baseURL             *url.URL
	proxy               *httputil.ReverseProxy
	httpClient          *http.Client
	streamingHTTPClient *http.Client
	agentThreadSessions AgentThreadSessionChannelStore
	agentStore          OwnedAgentStore
	workspaceStore      api.WorkspaceResolver
	logger              *slog.Logger
	now                 func() time.Time
	trustedJWTAudience  string
	billingEnabled      bool
	planStore           WorkspaceResourceLimitStore
}

func NewHandler(dependencies Dependencies) (*Handler, error) {
	baseURL, err := normalizeBaseURL(dependencies.BaseURL)
	if err != nil {
		return nil, err
	}
	handler := newHandlerCore(dependencies)
	handler.baseURL = baseURL
	transportClient := dependencies.HTTPClient
	if transportClient == nil {
		timeout := dependencies.RequestTimeout
		if timeout <= 0 {
			timeout = defaultProxyTimeout
		}
		transportClient = &http.Client{Timeout: timeout}
	}
	transport := transportClient.Transport
	if transport == nil {
		transport = http.DefaultTransport
	}
	handler.proxy = &httputil.ReverseProxy{
		Director:       handler.direct,
		Transport:      transport,
		ModifyResponse: handler.modifyForwardedResponse,
		ErrorHandler:   handler.handleProxyError,
	}
	handler.httpClient = transportClient
	handler.streamingHTTPClient = newMultipartStreamingHTTPClient(
		transportClient, defaultProxyTimeout,
	)
	return handler, nil
}

func newHandlerCore(dependencies Dependencies) *Handler {
	now := dependencies.Now
	if now == nil {
		now = time.Now
	}
	audience := strings.TrimSpace(dependencies.TrustedJWTAudience)
	if audience == "" {
		audience = "agentteam-upstream"
	}
	return &Handler{
		agentThreadSessions: dependencies.AgentThreadSessions,
		agentStore:          dependencies.AgentStore,
		workspaceStore:      dependencies.WorkspaceStore,
		logger:              dependencies.Logger,
		now:                 now,
		trustedJWTAudience:  audience,
		billingEnabled:      dependencies.BillingEnabled,
		planStore:           dependencies.PlanStore,
	}
}

func (handler *Handler) Proxy(ctx *gin.Context) {
	if handler == nil || handler.proxy == nil {
		api.Error(ctx, http.StatusServiceUnavailable, "API forwarder is not configured")
		return
	}
	authenticated, ok := api.AuthenticatedUserFromContext(ctx)
	if !ok {
		api.Error(ctx, http.StatusUnauthorized, "Unauthorized")
		return
	}
	jwt, err := handler.issueTrustedJWT(ctx, authenticated)
	if err != nil {
		api.Error(ctx, http.StatusInternalServerError, "Failed to prepare upstream identity")
		return
	}
	if !handler.requireSelfDemotionConfirmation(ctx, authenticated, jwt) {
		return
	}
	if !handler.requireMeshXResourceQuota(ctx, firstNonEmpty(authenticated.User.ID, authenticated.Session.UserID), jwt) {
		return
	}
	requestCtx := context.WithValue(ctx.Request.Context(), trustedJWTContextKey{}, jwt)
	if workspaceID, ok := validatedCurrentWorkspaceID(ctx); ok {
		requestCtx = context.WithValue(requestCtx, workspaceIDContextKey{}, workspaceID)
	}
	ctx.Request = ctx.Request.WithContext(requestCtx)
	handler.proxy.ServeHTTP(ctx.Writer, ctx.Request)
}

func (handler *Handler) ProxyDataPlane(ctx *gin.Context) {
	if handler == nil || handler.proxy == nil {
		api.Error(ctx, http.StatusServiceUnavailable, "API forwarder is not configured")
		return
	}
	handler.proxy.ServeHTTP(ctx.Writer, ctx.Request)
}

func (handler *Handler) ensureAgentThreadSessionChannel(ctx *gin.Context) {
	if handler == nil || handler.agentThreadSessions == nil {
		api.Error(ctx, http.StatusNotImplemented, "Agent thread session channel is not configured")
		return
	}
	authenticated, ok := api.AuthenticatedUserFromContext(ctx)
	if !ok {
		api.Error(ctx, http.StatusUnauthorized, "Unauthorized")
		return
	}
	workspaceID, ok := validatedCurrentWorkspaceID(ctx)
	if !ok {
		api.Error(ctx, http.StatusBadRequest, "No workspace selected")
		return
	}
	channel, err := handler.agentThreadSessions.EnsureAgentThreadSessionChannelForUser(ctx.Request.Context(), authenticated.User.ID, workspaceID)
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"channelId":   strings.TrimSpace(channel.ChannelID),
		"displayName": strings.TrimSpace(channel.DisplayName),
		"subsystem":   message.ChannelSubsystemAgentSession,
	})
}

func validatedCurrentWorkspaceID(ctx *gin.Context) (string, bool) {
	workspaceSummary, ok := api.CurrentWorkspaceFromContext(ctx)
	if !ok {
		return "", false
	}
	workspaceID := strings.TrimSpace(workspaceSummary.WorkspaceID)
	return workspaceID, workspaceID != ""
}

func (handler *Handler) RawRepositoryContent(ctx *gin.Context) {
	if handler == nil || handler.baseURL == nil {
		api.Error(ctx, http.StatusServiceUnavailable, "API forwarder is not configured")
		return
	}
	authenticated, ok := api.AuthenticatedUserFromContext(ctx)
	if !ok {
		api.Error(ctx, http.StatusUnauthorized, "Unauthorized")
		return
	}
	jwt, err := handler.issueTrustedJWT(ctx, authenticated)
	if err != nil {
		api.Error(ctx, http.StatusInternalServerError, "Failed to prepare upstream identity")
		return
	}
	handler.rawRepositoryContent(ctx, "Bearer "+jwt, jwt)
}

// RawRepositoryContentDataPlane preserves the repository credential so the
// embedded AGS authority, rather than the AgentTeam session middleware, authorizes
// the repository read.
func (handler *Handler) RawRepositoryContentDataPlane(ctx *gin.Context) {
	if handler == nil || handler.baseURL == nil {
		api.Error(ctx, http.StatusServiceUnavailable, "API forwarder is not configured")
		return
	}
	handler.rawRepositoryContent(ctx, ctx.GetHeader("Authorization"), "")
}

func (handler *Handler) rawRepositoryContent(ctx *gin.Context, authorization, trustedJWT string) {
	raw, err := repositoryroute.ParseRawRequest(ctx.Request)
	if err != nil {
		api.Error(ctx, http.StatusBadRequest, "Invalid raw repository path")
		return
	}

	contentsURL := *handler.baseURL
	contentsURL.Path = singleJoiningSlash(handler.baseURL.Path, "/api/v3/repos/"+url.PathEscape(raw.Owner)+"/"+url.PathEscape(raw.Repository)+"/contents/"+escapePathSegments(raw.FilePath))
	query := contentsURL.Query()
	query.Set("ref", raw.Ref)
	contentsURL.RawQuery = query.Encode()

	req, err := http.NewRequestWithContext(ctx.Request.Context(), http.MethodGet, contentsURL.String(), nil)
	if err != nil {
		api.Error(ctx, http.StatusInternalServerError, "Failed to prepare raw content request")
		return
	}
	req.Header.Set("Authorization", authorization)
	if trustedJWT != "" {
		req.Header.Set(trustedJWTHeader, "Bearer "+trustedJWT)
	}
	if routeWorkspaceID := strings.TrimSpace(ctx.GetHeader(api.HeaderRouteWorkspaceID)); routeWorkspaceID != "" {
		req.Header.Set(api.HeaderRouteWorkspaceID, routeWorkspaceID)
	}
	client := &http.Client{Timeout: defaultProxyTimeout}
	if handler.proxy != nil && handler.proxy.Transport != nil {
		client.Transport = handler.proxy.Transport
	}
	response, err := client.Do(req)
	if err != nil {
		handler.handleProxyError(ctx.Writer, ctx.Request, err)
		return
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		ctx.Status(response.StatusCode)
		_, _ = io.Copy(ctx.Writer, response.Body)
		return
	}
	var payload struct {
		Name     string `json:"name"`
		Type     string `json:"type"`
		Encoding string `json:"encoding"`
		Content  string `json:"content"`
	}
	if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
		api.Error(ctx, http.StatusBadGateway, "Invalid raw content response")
		return
	}
	if payload.Type != "file" || payload.Encoding != "base64" {
		api.Error(ctx, http.StatusBadRequest, "Raw path does not reference a file")
		return
	}
	content, err := base64.StdEncoding.DecodeString(stripBase64Whitespace(payload.Content))
	if err != nil {
		api.Error(ctx, http.StatusBadGateway, "Invalid raw content encoding")
		return
	}
	contentType := http.DetectContentType(content)
	ctx.Header("Content-Type", contentType)
	if payload.Name != "" {
		ctx.Header("Content-Disposition", "inline; filename=\""+strings.ReplaceAll(payload.Name, "\"", "")+"\"")
	}
	ctx.Status(http.StatusOK)
	_, _ = ctx.Writer.Write(content)
}

type trustedJWTContextKey struct{}
type workspaceIDContextKey struct{}

func (handler *Handler) direct(request *http.Request) {
	incoming := *request.URL
	originalHost := request.Host
	path, rawPath := joinURLPath(handler.baseURL, &incoming)
	request.URL.Scheme = handler.baseURL.Scheme
	request.URL.Host = handler.baseURL.Host
	request.URL.Path = path
	request.URL.RawPath = rawPath
	request.URL.RawQuery = incoming.RawQuery
	request.Host = handler.baseURL.Host
	request.Header = cloneHeader(request.Header)
	removeHopByHopHeaders(request.Header)
	removeAgentTeamInternalHeaders(request.Header)

	jwt, _ := request.Context().Value(trustedJWTContextKey{}).(string)
	if jwt != "" {
		request.Header.Set("Authorization", "Bearer "+jwt)
		request.Header.Set(trustedJWTHeader, "Bearer "+jwt)
	}
	if workspaceID, ok := request.Context().Value(workspaceIDContextKey{}).(string); ok && strings.TrimSpace(workspaceID) != "" {
		request.Header.Set(api.HeaderWorkspaceID, workspaceID)
	}
	if request.Header.Get("X-Forwarded-Host") == "" && originalHost != "" {
		request.Header.Set("X-Forwarded-Host", originalHost)
	}
	request.Header.Set("X-Forwarded-Proto", forwardedProto(request))
	appendXForwardedFor(request.Header, request.RemoteAddr)
}

func (handler *Handler) issueTrustedJWT(ctx *gin.Context, authenticated identity.AuthenticatedUser) (string, error) {
	claims, err := handler.trustedClaimsFromIdentity(TrustedIdentity{
		UserID:            firstNonEmpty(authenticated.User.ID, authenticated.Session.UserID),
		AgentTeamUserID:        authenticated.User.ID,
		Email:             authenticated.User.Email,
		Name:              authenticated.User.Name,
		PreferredUsername: trustedEmbeddedPreferredUsername(authenticated),
		DisplayName:       authenticated.User.DisplayName,
		AvatarURL:         authenticated.User.AvatarURL,
		SessionID:         authenticated.Session.ID,
	})
	if err != nil {
		return "", err
	}
	if workspaceSummary, ok := api.CurrentWorkspaceFromContext(ctx); ok {
		if workspaceID := strings.TrimSpace(workspaceSummary.WorkspaceID); workspaceID != "" {
			claims.WorkspaceIDs = []string{workspaceID}
			if hasForwarderPathPrefix(ctx.Request.URL.Path, "/api/v1/automations") {
				claims.AutomationPermissions = automationPermissionsForWorkspaceRole(workspaceSummary.WorkspaceRole)
			}
			if isAgentOwnedResourceForwarderPath(ctx.Request.URL.Path) {
				if err := handler.populateOwnedAgentClaims(ctx.Request.Context(), &claims, authenticated, workspaceID); err != nil {
					return "", err
				}
			}
		}
	}
	return encodeUnverifiedJWT(claims)
}

func (handler *Handler) populateOwnedAgentClaims(ctx context.Context, claims *trustedClaims, authenticated identity.AuthenticatedUser, workspaceID string) error {
	if handler.agentStore == nil {
		return nil
	}
	agents, err := handler.agentStore.ListOwnedAgentsForWorkspace(ctx, firstNonEmpty(authenticated.User.ID, authenticated.Session.UserID), workspaceID)
	if err != nil {
		return fmt.Errorf("load owned agents for MeshX resource route: %w", err)
	}
	claims.AgentOwnershipWorkspaceID = workspaceID
	claims.OwnedAgents = make([]TrustedOwnedAgent, 0, len(agents))
	claims.OwnedAgentIDs = make([]string, 0, len(agents))
	for _, agent := range agents {
		id := strings.TrimSpace(agent.ID)
		if id == "" {
			continue
		}
		claims.OwnedAgentIDs = append(claims.OwnedAgentIDs, id)
		claims.OwnedAgents = append(claims.OwnedAgents, TrustedOwnedAgent{
			ID:          id,
			Name:        strings.TrimSpace(agent.Name),
			DisplayName: strings.TrimSpace(agent.DisplayName),
		})
	}
	return nil
}

func isAgentOwnedResourceForwarderPath(path string) bool {
	path = strings.TrimSpace(path)
	kbAgentControlPath := hasForwarderPathPrefix(path, "/api/v1/kb") &&
		(hasForwarderPathPrefix(path, "/api/v1/kb/agents") || strings.Contains(path, "/permissions"))
	return kbAgentControlPath ||
		hasForwarderPathPrefix(path, "/api/v1/memory") ||
		hasForwarderPathPrefix(path, "/api/v1/skills") ||
		hasForwarderPathPrefix(path, "/api/v1/roles") ||
		strings.HasPrefix(path, "/api/v1/roles:")
}

func hasForwarderPathPrefix(path string, prefix string) bool {
	return path == prefix || strings.HasPrefix(path, prefix+"/")
}

type trustedClaims struct {
	Issuer                    string              `json:"iss"`
	Audience                  string              `json:"aud"`
	Subject                   string              `json:"sub"`
	AgentTeamUserID                string              `json:"agentteam_user_id,omitempty"`
	Email                     string              `json:"email,omitempty"`
	Name                      string              `json:"name,omitempty"`
	PreferredUsername         string              `json:"preferred_username,omitempty"`
	DisplayName               string              `json:"display_name,omitempty"`
	AvatarURL                 string              `json:"picture,omitempty"`
	SessionID                 string              `json:"sid,omitempty"`
	WorkspaceIDs              []string            `json:"workspace_ids,omitempty"`
	AutomationPermissions     []string            `json:"automation_permissions,omitempty"`
	AgentOwnershipWorkspaceID string              `json:"agent_ownership_workspace_id,omitempty"`
	OwnedAgentIDs             []string            `json:"owned_agent_ids,omitempty"`
	OwnedAgents               []TrustedOwnedAgent `json:"owned_agents,omitempty"`
	InternalPurpose           string              `json:"agentteam_internal_purpose,omitempty"`
	IssuedAt                  int64               `json:"iat"`
	ExpiresAt                 int64               `json:"exp"`
}

func automationPermissionsForWorkspaceRole(role string) []string {
	switch domain.WorkspaceRole(strings.TrimSpace(role)) {
	case domain.WorkspaceRoleOwner, domain.WorkspaceRoleAdmin:
		return []string{"automation:*"}
	}
	permissions := []string{
		"automation:repo:read",
		"automation:run:read",
		"automation:run:start",
		"automation:wait:resume",
	}
	return permissions
}

func encodeUnverifiedJWT(claims trustedClaims) (string, error) {
	headerJSON, err := json.Marshal(map[string]string{"alg": "none", "typ": "JWT"})
	if err != nil {
		return "", fmt.Errorf("encode trusted JWT header: %w", err)
	}
	payloadJSON, err := json.Marshal(claims)
	if err != nil {
		return "", fmt.Errorf("encode trusted JWT claims: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(headerJSON) + "." + base64.RawURLEncoding.EncodeToString(payloadJSON) + ".", nil
}

func trustedEmbeddedPreferredUsername(authenticated identity.AuthenticatedUser) string {
	for _, candidate := range []string{
		normalizePreferredUsernameCandidate(authenticated.User.Name),
		normalizePreferredUsernameCandidate(emailLocalPart(authenticated.User.Email)),
		normalizePreferredUsernameCandidate(authenticated.User.ID),
		normalizePreferredUsernameCandidate(authenticated.Session.UserID),
	} {
		if candidate != "" {
			return candidate
		}
	}
	return ""
}

func emailLocalPart(email string) string {
	before, _, ok := strings.Cut(strings.TrimSpace(email), "@")
	if !ok {
		return strings.TrimSpace(email)
	}
	return before
}

func normalizePreferredUsernameCandidate(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return ""
	}
	var builder strings.Builder
	lastHyphen := false
	for _, r := range value {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
			builder.WriteRune(unicode.ToLower(r))
			lastHyphen = false
		case r == '-', r == '_', r == '.', unicode.IsSpace(r):
			if builder.Len() > 0 && !lastHyphen {
				builder.WriteByte('-')
				lastHyphen = true
			}
		}
	}
	return strings.Trim(builder.String(), "-")
}

func normalizeBaseURL(raw string) (*url.URL, error) {
	trimmed := strings.TrimRight(strings.TrimSpace(raw), "/")
	if trimmed == "" {
		return nil, errors.New("forwarder base URL is not configured")
	}
	parsed, err := url.Parse(trimmed)
	if err != nil {
		return nil, fmt.Errorf("parse forwarder base URL: %w", err)
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return nil, errors.New("forwarder base URL must use http(s)")
	}
	if parsed.Host == "" {
		return nil, errors.New("forwarder base URL must include host")
	}
	return parsed, nil
}

func (handler *Handler) modifyForwardedResponse(response *http.Response) error {
	if response == nil {
		return nil
	}
	removeHopByHopHeaders(response.Header)
	if response.StatusCode >= http.StatusInternalServerError {
		handler.logUpstreamError(response)
	}
	return nil
}

func (handler *Handler) logUpstreamError(response *http.Response) {
	if handler == nil || handler.logger == nil || response == nil {
		return
	}
	ctx := context.Background()
	attrs := []slog.Attr{slog.Int("status", response.StatusCode)}
	if response.Request != nil {
		ctx = response.Request.Context()
		attrs = append(attrs, slog.String("method", response.Request.Method))
		if response.Request.URL != nil {
			attrs = append(attrs, slog.String("path", response.Request.URL.Path))
		}
	}
	if preview := previewForwardedResponseBody(response); preview != "" {
		attrs = append(attrs, slog.String("body_preview", preview))
	}
	handler.logger.LogAttrs(ctx, slog.LevelWarn, "API forwarder upstream returned error", attrs...)
}

func previewForwardedResponseBody(response *http.Response) string {
	if response == nil || response.Body == nil || !isTextualContentType(response.Header.Get("Content-Type")) {
		return ""
	}
	body, err := io.ReadAll(io.LimitReader(response.Body, 4096))
	if len(body) > 0 {
		response.Body = struct {
			io.Reader
			io.Closer
		}{
			Reader: io.MultiReader(bytes.NewReader(body), response.Body),
			Closer: response.Body,
		}
	}
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(body))
}

func isTextualContentType(contentType string) bool {
	contentType = strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
	return contentType == "application/json" ||
		strings.HasSuffix(contentType, "+json") ||
		strings.HasPrefix(contentType, "text/")
}

func (handler *Handler) handleProxyError(writer http.ResponseWriter, request *http.Request, err error) {
	if handler.logger != nil {
		handler.logger.WarnContext(request.Context(), "API forwarder request failed", slog.String("path", request.URL.Path), slog.Any("error", err))
	}
	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
	writer.WriteHeader(http.StatusBadGateway)
	_, _ = writer.Write([]byte(`{"error":"API forwarder request failed"}`))
}

func stripBase64Whitespace(value string) string {
	return strings.Map(func(r rune) rune {
		switch r {
		case ' ', '\n', '\r', '\t':
			return -1
		default:
			return r
		}
	}, value)
}

func escapePathSegments(path string) string {
	parts := strings.Split(path, "/")
	for i, part := range parts {
		parts[i] = url.PathEscape(part)
	}
	return strings.Join(parts, "/")
}

func isSmartHTTPGitPath(path string) bool {
	trimmed := strings.Trim(path, "/")
	if trimmed == "" {
		return false
	}
	parts := strings.Split(trimmed, "/")
	if len(parts) < 2 {
		return false
	}
	return strings.HasSuffix(parts[1], ".git")
}

func isAGSRepositoryRESTDataPlaneRequest(request *http.Request) bool {
	_, _, repositoryDataPlane := agsserver.RepositoryAuthorizationTarget(request)
	_, _, repositoryIDDataPlane := agsserver.RepositoryIDAuthorizationTarget(request)
	return (repositoryDataPlane || repositoryIDDataPlane) && request != nil && hasAGSRepositoryCredential(request.Header.Get("Authorization"))
}

func hasAGSRepositoryCredential(raw string) bool {
	fields := strings.Fields(strings.TrimSpace(raw))
	if len(fields) != 2 || strings.TrimSpace(fields[1]) == "" {
		return false
	}
	switch strings.ToLower(fields[0]) {
	case "basic", "token":
		return true
	case "bearer":
		return strings.HasPrefix(fields[1], "ags_rat_")
	default:
		return false
	}
}

func cloneHeader(header http.Header) http.Header {
	cloned := make(http.Header, len(header))
	for key, values := range header {
		cloned[key] = append([]string(nil), values...)
	}
	return cloned
}

func removeAgentTeamInternalHeaders(header http.Header) {
	prefix := strings.Join([]string{"X", "AgentTeam", ""}, "-")
	for key := range header {
		if strings.HasPrefix(http.CanonicalHeaderKey(key), prefix) {
			header.Del(key)
		}
	}
}

var hopByHopHeaders = []string{
	"Connection",
	"Proxy-Connection",
	"Keep-Alive",
	"Proxy-Authenticate",
	"Proxy-Authorization",
	"Te",
	"Trailer",
	"Transfer-Encoding",
	"Upgrade",
}

func removeHopByHopHeaders(header http.Header) {
	for _, key := range hopByHopHeaders {
		header.Del(key)
	}
	for _, value := range header.Values("Connection") {
		for _, field := range strings.Split(value, ",") {
			if trimmed := strings.TrimSpace(field); trimmed != "" {
				header.Del(trimmed)
			}
		}
	}
}

func appendXForwardedFor(header http.Header, remoteAddr string) {
	host, _, err := net.SplitHostPort(remoteAddr)
	if err != nil {
		host = remoteAddr
	}
	host = strings.TrimSpace(host)
	if host == "" {
		return
	}
	if prior := header.Get("X-Forwarded-For"); prior != "" {
		header.Set("X-Forwarded-For", prior+", "+host)
		return
	}
	header.Set("X-Forwarded-For", host)
}

func forwardedProto(request *http.Request) string {
	if request.TLS != nil {
		return "https"
	}
	return "http"
}

func singleJoiningSlash(basePath string, requestPath string) string {
	baseHasSlash := strings.HasSuffix(basePath, "/")
	requestHasSlash := strings.HasPrefix(requestPath, "/")
	switch {
	case baseHasSlash && requestHasSlash:
		return basePath + requestPath[1:]
	case !baseHasSlash && !requestHasSlash:
		return basePath + "/" + requestPath
	default:
		return basePath + requestPath
	}
}

func joinURLPath(base *url.URL, request *url.URL) (string, string) {
	if base.RawPath == "" && request.RawPath == "" {
		return singleJoiningSlash(base.Path, request.Path), ""
	}
	basePath := base.EscapedPath()
	requestPath := request.EscapedPath()
	baseHasSlash := strings.HasSuffix(basePath, "/")
	requestHasSlash := strings.HasPrefix(requestPath, "/")
	switch {
	case baseHasSlash && requestHasSlash:
		return base.Path + request.Path[1:], basePath + requestPath[1:]
	case !baseHasSlash && !requestHasSlash:
		return base.Path + "/" + request.Path, basePath + "/" + requestPath
	default:
		return base.Path + request.Path, basePath + requestPath
	}
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}
