package exec

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agentview"
)

const (
	maxActionBodyBytes        = 1 << 20
	maxExecBearerBytes        = 16 << 10
	execActionBodyReadTimeout = 5 * time.Second
)

// AgentReader supplies the local Agent projection without coupling Exec to a
// sibling HTTP handler package.
type AgentReader interface {
	// ListAgents returns the current bounded local Agent projection.
	ListAgents(context.Context) ([]agentview.Snapshot, error)
	// GetAgent returns one projected Agent by id.
	GetAgent(context.Context, string) (agentview.Snapshot, bool, error)
}

// agentPageReader is the production fast path for cursor reads. Legacy test
// and embedding readers may keep implementing AgentReader; production readers
// use this optional interface to avoid materializing every Agent snapshot.
type agentPageReader interface {
	ListAgentsPage(context.Context, string, string, string, int) ([]agentview.Snapshot, bool, string, error)
}

// agentPageReaderWithAnchor is the bounded production fast path. The anchor
// can be a non-matching indexed entry when a sparse query consumes the scan
// budget, so the handler must not manufacture it from the returned page.
type agentPageReaderWithAnchor interface {
	ListAgentsPageWithAnchor(context.Context, string, string, string, int) ([]agentview.Snapshot, bool, string, string, error)
}

// TurnTargetReader supplies the daemon-authoritative target reference of the
// current turn's triggering delivery for one agent. It is read per request
// (never frozen into a process-lifetime authorization) so conversation-scoped
// defaults follow the turn the caller is actually serving. Empty means
// "unknown" and consumers must fail closed.
type TurnTargetReader interface {
	// CurrentTurnDeliveryTarget returns the canonical target reference of the
	// current turn delivery served by the process that owns the given Exec
	// authorization, or "" when the authorization does not match the session's
	// current process generation or no turn target is known. The
	// authorization fence prevents a replaced process's still-valid Exec
	// credential from reading its successor's turn target.
	CurrentTurnDeliveryTarget(agentID, authorizationID string) string
}

// RouteDependencies are the narrow services used by Exec route assembly.
type RouteDependencies struct {
	Authorizations *RuntimeAuthorizationStore
	Agents         AgentReader
	TurnTargets    TurnTargetReader
}

// actionScopedJSON is the only request shape received by concrete domain
// methods. Body is model-authored; Authorized is daemon-authored.
type actionScopedJSON[I any] struct {
	Authorized runtimeAuthorization
	Body       I
}

type adapter struct {
	authorizer interface {
		acquire(context.Context, string, string, string) (runtimeAuthorization, func(), error)
	}
	agents          AgentReader
	catalog         *Catalog
	builtin         map[string]builtinInvoker
	setReadDeadline func(http.ResponseWriter, time.Time) error
	bodyReadTimeout time.Duration
}

type builtinInvokerFunc func(
	context.Context,
	runtimeAuthorization,
	json.RawMessage,
) ([]byte, error)

type builtinInvoker struct {
	invoke     builtinInvokerFunc
	capability builtinInvokerCapability
	// executable is the concrete transport boundary from route assembly. The
	// registered capability is derived from it and validated again at startup;
	// retaining both lets construction fail closed if a test or future caller
	// swaps only one side.
	executable builtinExecutableTransport
}

func (invoker builtinInvoker) call(ctx context.Context, authorized runtimeAuthorization, raw json.RawMessage) ([]byte, error) {
	if invoker.invoke == nil {
		return nil, NewError(CodeInternal, "internal error")
	}
	return invoker.invoke(ctx, authorized, raw)
}

// RegisterRoutes installs the bearer-bound merged Catalog and one dynamic Exec
// action route. Built-ins retain their typed handlers behind the map.
func RegisterRoutes(group *gin.RouterGroup, dependencies RouteDependencies) {
	catalog, err := DefaultCatalog()
	if err != nil {
		panic(err)
	}
	adapter := adapter{
		authorizer: dependencies.Authorizations, agents: dependencies.Agents, catalog: catalog,
		builtin: make(map[string]builtinInvoker, catalog.len()),
		setReadDeadline: func(writer http.ResponseWriter, deadline time.Time) error {
			return http.NewResponseController(writer).SetReadDeadline(deadline)
		},
		bodyReadTimeout: execActionBodyReadTimeout,
	}
	agent := agentHandler{
		source:   dependencies.Agents,
		local:    localReadTransport{source: dependencies.Agents},
		localGet: localReadTransport{source: dependencies.Agents, hasNotFound: true},
	}
	team := teamHandler{}
	channel := channelHandler{}
	profile := profileHandler{}
	project := projectHandler{}
	review := reviewHandler{}
	role := roleHandler{}
	skill := skillHandler{}
	scheduled := scheduledMessageHandler{}
	task := taskHandler{turnTargets: dependencies.TurnTargets}
	workflow := workflowHandler{}
	workspace := workspaceHandler{}

	for _, registration := range builtinRouteRegistrations(agent, team, channel, profile, project, review, role, skill, scheduled, task, workflow, workspace) {
		if err := registration.validate(); err != nil {
			panic(err)
		}
		registration.Install(adapter)
	}
	if err := validateBuiltinActionInvokers(catalog, adapter.builtin); err != nil {
		panic(err)
	}
	group.GET("/servers/:serverID/agents/:agentID/catalog", handleRuntimeCatalog(adapter))
	group.POST("/servers/:serverID/agents/:agentID/:domain/:action", handleRuntimeAction(adapter))
}

type typedActionHandler[I, O any] func(context.Context, actionScopedJSON[I]) (actionResult[O], error)

func registerJSON[I, O any](
	group *gin.RouterGroup,
	adapter adapter,
	domain string,
	action string,
	handler typedActionHandler[I, O],
	capabilities ...builtinInvokerCapability,
) {
	if len(capabilities) > 1 {
		panic("multiple built-in Exec invoker capabilities " + domain + " " + action)
	}
	capability := builtinInvokerCapability{}
	if len(capabilities) == 1 {
		capability = capabilities[0]
	}
	// Compatibility helper retained only for narrowly scoped route tests. The
	// production registry has no handler/transport pairing API.
	registerBuiltinAction(group, adapter, domain, action, nil, capability, func(ctx context.Context, authorized runtimeAuthorization, raw json.RawMessage) ([]byte, error) {
		input, err := decodeValidatedActionInput[I](adapter.catalog, domain, action, raw)
		if err != nil {
			return nil, err
		}
		result, err := handler(ctx, actionScopedJSON[I]{Authorized: authorized, Body: input})
		if err != nil {
			return nil, err
		}
		return marshalTypedActionResultForActionWithInput(domain, action, raw, result)
	})
}

func registerJSONWithOwnedInvoker[I, O any](
	group *gin.RouterGroup,
	adapter adapter,
	domain string,
	action string,
	invoker builtinExecutableInvoker[I, O],
	attested builtinInvokerCapability,
) {
	capability := attested.withRoute(domain, action)
	actual := invoker.Capability().withRoute(domain, action)
	if !capabilityEqual(capability, actual) {
		panic("owned executable invoker capability mismatch for " + domain + " " + action)
	}
	registerBuiltinAction(group, adapter, domain, action, invoker.Executable(), capability, func(ctx context.Context, authorized runtimeAuthorization, raw json.RawMessage) ([]byte, error) {
		input, err := decodeValidatedActionInput[I](adapter.catalog, domain, action, raw)
		if err != nil {
			return nil, err
		}
		result, err := invoker.Invoke(ctx, authorized, input)
		if err != nil {
			return nil, err
		}
		return marshalTypedActionResultForActionWithInput(domain, action, raw, result)
	})
}

func registerBuiltinAction(
	group *gin.RouterGroup,
	adapter adapter,
	domain, action string,
	executable builtinExecutableTransport,
	capability builtinInvokerCapability,
	invoke builtinInvokerFunc,
) {
	if adapter.builtin == nil {
		adapter.builtin = make(map[string]builtinInvoker)
	}
	key := domain + " " + action
	if adapter.builtin[key].invoke != nil {
		panic("duplicate built-in Exec invoker " + key)
	}
	adapter.builtin[key] = builtinInvoker{executable: executable, invoke: invoke, capability: capability}
	if group != nil {
		group.POST(
			"/servers/:serverID/agents/:agentID/"+domain+"/"+action,
			handleBuiltinAction(adapter, domain, action),
		)
	}
}

func handleBuiltinAction(adapter adapter, domain, action string) gin.HandlerFunc {
	return func(c *gin.Context) {
		authorized, release, ok := acquireRuntimeRequest(c, adapter, "action")
		if !ok {
			return
		}
		defer release()
		requestContext, stopWatch := watchRuntimeRevocation(c, authorized.revoked)
		defer stopWatch()
		raw, err := readAuthorizedActionBody(c, adapter, authorized.revoked)
		if err != nil {
			if execAuthorizationRevoked(authorized.revoked) {
				writeError(c, ApplyErrorBudget(NewError(CodePermissionDenied, "Exec action scope mismatch")))
			} else {
				writeError(c, ApplyErrorBudget(err))
			}
			return
		}
		if err := validateReplayHeaders(domain, action, raw, c.GetHeader("Idempotency-Key"), c.GetHeader("X-Request-ID")); err != nil {
			writeError(c, ApplyErrorBudget(err))
			return
		}
		invoker, exists := adapter.builtin[domain+" "+action]
		if !exists || invoker.invoke == nil {
			writeError(c, ApplyErrorBudget(NewError(CodeInternal, "internal error")))
			return
		}
		encoded, err := invoker.call(requestContext, authorized, raw)
		if err != nil {
			if execAuthorizationRevoked(authorized.revoked) || errors.Is(err, context.Canceled) {
				writeError(c, ApplyErrorBudget(NewError(CodePermissionDenied, "Exec action scope mismatch")))
			} else {
				writeError(c, ApplyErrorBudget(err))
			}
			return
		}
		if execAuthorizationRevoked(authorized.revoked) {
			writeError(c, ApplyErrorBudget(NewError(CodePermissionDenied, "Exec action scope mismatch")))
			return
		}
		c.Data(http.StatusOK, "application/json", encoded)
	}
}

func decodeValidatedActionInput[I any](catalog *Catalog, domain, action string, raw []byte) (I, error) {
	var input I
	if domain == "workflow" {
		if err := validateWorkflowActionJSON(raw); err != nil {
			return input, &Error{
				Code: CodeValidation, Problem: "invalid workflow " + action + " request",
				Cause: err,
			}
		}
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&input); err != nil {
		return input, &Error{
			Code: CodeValidation, Problem: "invalid " + domain + " " + action + " request",
			Hint:  "Check `agentteam exec " + domain + " " + action + " --input-schema` and fix the JSON input.",
			Cause: err,
		}
	}
	if err := decoder.Decode(new(struct{})); err != io.EOF {
		return input, &Error{
			Code: CodeValidation, Problem: "invalid " + domain + " " + action + " request",
			Cause: errors.New("unexpected trailing JSON"),
		}
	}
	if catalog != nil {
		if err := catalog.ValidateInput(domain, action, raw); err != nil {
			return input, &Error{Code: CodeValidation, Problem: "invalid input", Cause: err}
		}
	}
	return input, nil
}

// readAuthorizedActionBody owns the authenticated HTTP-body lifetime. A
// successful complete read clears the connection deadline; every failure keeps
// it in place so net/http cannot park shutdown while draining a slow sender.
func readAuthorizedActionBody(c *gin.Context, adapter adapter, revoked <-chan struct{}) ([]byte, error) {
	readDeadlineSet := false
	if adapter.setReadDeadline != nil {
		timeout := adapter.bodyReadTimeout
		if timeout <= 0 {
			timeout = execActionBodyReadTimeout
		}
		if err := adapter.setReadDeadline(c.Writer, time.Now().Add(timeout)); err != nil {
			return nil, NewError(CodeInternal, "internal error")
		}
		readDeadlineSet = true
	}

	raw, err := readActionBody(c.Request.Body)
	if err != nil {
		return nil, err
	}
	if readDeadlineSet {
		if err := adapter.setReadDeadline(c.Writer, time.Time{}); err != nil {
			return nil, NewError(CodeInternal, "internal error")
		}
	}
	if execAuthorizationRevoked(revoked) {
		return nil, NewError(CodePermissionDenied, "Exec action scope mismatch")
	}
	return raw, nil
}

func execAuthorizationRevoked(revoked <-chan struct{}) bool {
	if revoked == nil {
		return false
	}
	select {
	case <-revoked:
		return true
	default:
		return false
	}
}

// marshalTypedActionResult is retained for package-local callers that do not
// have an action identity. Production routes use
// marshalTypedActionResultForAction so oversize results return a typed error.
// A nil byte slice means no success envelope was produced.
func marshalTypedActionResult[O any](result actionResult[O]) []byte {
	encoded, _ := marshalTypedActionResultForAction("", "", result)
	return encoded
}

func strictBearer(header string) (string, error) {
	if len(header) > len("Bearer ")+maxExecBearerBytes {
		return "", NewError(CodePermissionDenied, "strict Bearer token required")
	}
	header = strings.TrimSpace(header)
	if !strings.HasPrefix(header, "Bearer ") || strings.Count(header, " ") != 1 {
		return "", NewError(CodePermissionDenied, "strict Bearer token required")
	}
	tokenValue := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
	if tokenValue == "" || len(tokenValue) > maxExecBearerBytes {
		return "", NewError(CodePermissionDenied, "strict Bearer token required")
	}
	return tokenValue, nil
}

func readActionBody(body io.Reader) (json.RawMessage, error) {
	raw, err := io.ReadAll(io.LimitReader(body, maxActionBodyBytes+1))
	if err != nil {
		return nil, &Error{Code: CodeValidation, Problem: "invalid Exec action request", Cause: err}
	}
	raw = bytes.TrimSpace(raw)
	if len(raw) == 0 || len(raw) > maxActionBodyBytes || !json.Valid(raw) {
		return nil, NewError(CodeValidation, "invalid Exec action request")
	}
	return append(json.RawMessage(nil), raw...), nil
}

func errorResponsePayload(envelope ErrorEnvelope) map[string]ErrorEnvelope {
	return map[string]ErrorEnvelope{"error": envelope}
}

func writeError(c *gin.Context, envelope ErrorEnvelope) {
	c.AbortWithStatusJSON(envelope.Code.httpStatus(), errorResponsePayload(envelope))
}
