package api

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

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"

	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

const defaultInvalidJSONMessage = "Invalid request body"

// PublicRequest is the typed context passed to routes that do not require an
// authenticated principal.
type PublicRequest struct {
	Gin *gin.Context
}

// AuthenticatedRequest is the typed context passed to auth-scoped handlers.
type AuthenticatedRequest struct {
	Gin  *gin.Context
	User identity.AuthenticatedUser
}

// WorkspaceScopedRequest is the typed context passed to handlers that require a
// current workspace. It embeds AuthenticatedRequest because every
// workspace-scoped request has already passed authentication middleware.
type WorkspaceScopedRequest struct {
	AuthenticatedRequest
	Workspace workspace.Summary
}

// AgentScopedRequest is the typed context passed to handlers below an
// authenticated Agent Runtime route.
type AgentScopedRequest struct {
	Gin       *gin.Context
	Principal agent.AuthenticatedAgent
}

// AuthenticatedJSON is the typed context passed to auth-scoped JSON
// handlers. The request scope stays non-generic; Body is the decoded JSON
// payload for this endpoint.
type AuthenticatedJSON[T any] struct {
	AuthenticatedRequest
	Body T
}

// WorkspaceScopedJSON is the typed context passed to workspace-scoped
// JSON handlers. It embeds WorkspaceScopedRequest so workspace handlers keep
// the same accessors while receiving a typed JSON body.
type WorkspaceScopedJSON[T any] struct {
	WorkspaceScopedRequest
	Body T
}

// AgentScopedJSON is the typed context passed to Agent Runtime JSON handlers.
type AgentScopedJSON[T any] struct {
	AgentScopedRequest
	Body T
}

// Context returns the underlying request context.
func (request AuthenticatedRequest) Context() context.Context {
	return ginRequestContext(request.Gin)
}

// UserID returns the authenticated user ID.
func (request AuthenticatedRequest) UserID() string {
	return request.User.User.ID
}

// WorkspaceID returns the current workspace ID.
func (request WorkspaceScopedRequest) WorkspaceID() string {
	return request.Workspace.WorkspaceID
}

// Context returns the underlying Agent Runtime request context.
func (request AgentScopedRequest) Context() context.Context {
	return ginRequestContext(request.Gin)
}

func ginRequestContext(ctx *gin.Context) context.Context {
	if ctx != nil && ctx.Request != nil {
		if requestContext := ctx.Request.Context(); requestContext != nil {
			return requestContext
		}
	}
	return context.Background()
}

// Response describes the HTTP response a typed handler wants the adapter to write.
type Response struct {
	Status      int
	Body        any
	NoBody      bool
	ContentType string
	Headers     http.Header
	ContentName string
	Content     io.ReadSeeker
	ModifiedAt  time.Time
}

// OK returns a 200 JSON response.
func OK(body any) Response {
	return Response{Status: http.StatusOK, Body: body}
}

// Created returns a 201 JSON response.
func Created(body any) Response {
	return Response{Status: http.StatusCreated, Body: body}
}

// NoContent returns a 204 response without a JSON body.
func NoContent() Response {
	return Response{Status: http.StatusNoContent, NoBody: true}
}

// Content returns a seekable byte response with standard HTTP range and
// conditional-request handling. Streaming details remain in this adapter so
// domain handlers do not need to accept *gin.Context.
func Content(status int, name string, contentType string, content io.ReadSeeker, modifiedAt time.Time, headers http.Header) Response {
	return Response{
		Status:      status,
		ContentType: contentType,
		Headers:     headers,
		ContentName: name,
		Content:     content,
		ModifiedAt:  modifiedAt,
	}
}

// HTTPError is a generic API-layer error with an explicit HTTP status/message.
type HTTPError struct {
	Status  int
	Message string
	Code    string
	Cause   error
}

func (err HTTPError) Error() string {
	message := strings.TrimSpace(err.Message)
	if message == "" {
		message = http.StatusText(err.Status)
	}
	if message == "" {
		message = "Request failed"
	}
	return message
}

func (err HTTPError) Unwrap() error {
	return err.Cause
}

// NewHTTPError creates a generic API-layer error with an explicit status/message.
func NewHTTPError(status int, message string) error {
	return HTTPError{Status: status, Message: message}
}

// NewHTTPErrorWithCode creates a generic API-layer error with a stable error code.
func NewHTTPErrorWithCode(status int, message string, code string) error {
	return HTTPError{Status: status, Message: message, Code: code}
}

// NewHTTPErrorWithCause creates an API-layer error that returns a safe message
// to clients while retaining the underlying cause for logs and traces.
func NewHTTPErrorWithCause(status int, message string, cause error) error {
	return HTTPError{Status: status, Message: message, Cause: cause}
}

// NewHTTPErrorWithCodeAndCause creates a coded API-layer error with an
// underlying cause for logs and traces.
func NewHTTPErrorWithCodeAndCause(status int, message string, code string, cause error) error {
	return HTTPError{Status: status, Message: message, Code: code, Cause: cause}
}

// ErrorResponse is the normalized result of classifying a domain error.
type ErrorResponse struct {
	Status  int
	Message string
	Code    string
	Details any

	// Canonical public Workflow contract fields. Only workflow domain
	// classifiers populate them; every other domain leaves them empty so the
	// shared response writer emits the same envelope as before.
	Stage       string
	Path        string
	Hint        string
	Retryable   bool
	Diagnostics any
	Truncation  any
}

// ErrorClassifier maps domain errors to API responses. It should be owned by
// the domain package that understands those errors.
type ErrorClassifier func(error) (ErrorResponse, bool)

type handlerOptions struct {
	invalidJSONMessage string
	classifiers        []ErrorClassifier
	strict             bool
	agentSuccess       AgentSuccessHandler
}

// AgentSuccessHandler runs after an Agent-scoped handler has completed without
// an error and before its response is written. Returning an error aborts the
// success response; this makes durable post-success accounting part of the
// request's final commit instead of best-effort telemetry. It is intentionally
// attached to the shared adapter rather than individual actions so
// cross-cutting success accounting cannot drift between commands.
type AgentSuccessHandler func(AgentScopedRequest, Response) error

// HandlerOption customizes typed handler adapters without creating a central
// domain error registry in the root api package.
type HandlerOption func(*handlerOptions)

// WithErrorClassifier lets a domain package supply local error mapping.
func WithErrorClassifier(classifier ErrorClassifier) HandlerOption {
	return func(options *handlerOptions) {
		if classifier != nil {
			options.classifiers = append(options.classifiers, classifier)
		}
	}
}

// WithInvalidJSONMessage preserves legacy endpoint-specific invalid-body text
// while new handlers default to "Invalid request body".
func WithInvalidJSONMessage(message string) HandlerOption {
	return func(options *handlerOptions) {
		if trimmed := strings.TrimSpace(message); trimmed != "" {
			options.invalidJSONMessage = trimmed
		}
	}
}

// WithStrictJSON makes typed JSON binding reject unknown fields (HTTP 400) so a
// canonical contract does not silently swallow legacy fields the contract has
// removed. Apply it only to canonical routes; compatibility boundaries stay
// lenient.
func WithStrictJSON() HandlerOption {
	return func(options *handlerOptions) {
		options.strict = true
	}
}

// WithAgentSuccess installs one post-handler success hook for an
// Agent-scoped route. The hook is not called for authentication, decoding,
// validation, authorization, business, or response errors. A hook error is
// classified and written through the same error path as the route handler.
func WithAgentSuccess(handler AgentSuccessHandler) HandlerOption {
	return func(options *handlerOptions) {
		options.agentSuccess = handler
	}
}

func newHandlerOptions(opts []HandlerOption) handlerOptions {
	options := handlerOptions{invalidJSONMessage: defaultInvalidJSONMessage}
	for _, opt := range opts {
		if opt != nil {
			opt(&options)
		}
	}
	return options
}

// NewAuthenticatedRequest reads the authenticated user set by existing auth middleware.
func NewAuthenticatedRequest(ctx *gin.Context) (AuthenticatedRequest, error) {
	authenticated, ok := AuthenticatedUserFromContext(ctx)
	if !ok {
		return AuthenticatedRequest{}, NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}
	return AuthenticatedRequest{Gin: ctx, User: authenticated}, nil
}

// NewWorkspaceScopedRequest reads authenticated user and current workspace values set
// by existing middleware.
func NewWorkspaceScopedRequest(ctx *gin.Context) (WorkspaceScopedRequest, error) {
	authenticated, err := NewAuthenticatedRequest(ctx)
	if err != nil {
		return WorkspaceScopedRequest{}, err
	}
	workspaceSummary, ok := CurrentWorkspaceFromContext(ctx)
	if !ok {
		return WorkspaceScopedRequest{}, NewHTTPError(http.StatusBadRequest, "No workspace selected")
	}
	return WorkspaceScopedRequest{AuthenticatedRequest: authenticated, Workspace: workspaceSummary}, nil
}

// NewAgentScopedRequest reads the Agent principal set by Agent Runtime
// authentication middleware. Authentication and route consistency checks stay
// in middleware so each request is authenticated exactly once.
func NewAgentScopedRequest(ctx *gin.Context) (AgentScopedRequest, error) {
	authenticated, ok := AuthenticatedAgentFromContext(ctx)
	if !ok {
		return AgentScopedRequest{}, NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}
	return AgentScopedRequest{Gin: ctx, Principal: authenticated}, nil
}

// PublicHandler is a typed handler for routes that do not require an authenticated principal.
type PublicHandler func(PublicRequest) (Response, error)

// AuthHandler is a typed handler that requires an authenticated user.
type AuthHandler func(AuthenticatedRequest) (Response, error)

// WorkspaceScopedHandler is a typed handler that requires an authenticated user and current workspace.
type WorkspaceScopedHandler func(WorkspaceScopedRequest) (Response, error)

// AgentScopedHandler is a typed handler below Agent Runtime authentication.
type AgentScopedHandler func(AgentScopedRequest) (Response, error)

// HandlePublic adapts an unauthenticated typed handler to gin.HandlerFunc.
func HandlePublic(handler PublicHandler, opts ...HandlerOption) gin.HandlerFunc {
	options := newHandlerOptions(opts)
	return func(ctx *gin.Context) {
		response, err := handler(PublicRequest{Gin: ctx})
		if err != nil {
			writeHandlerError(ctx, err, options)
			return
		}
		writeResponse(ctx, response)
	}
}

// HandleAuth adapts an auth-scoped typed handler to gin.HandlerFunc.
func HandleAuth(handler AuthHandler, opts ...HandlerOption) gin.HandlerFunc {
	options := newHandlerOptions(opts)
	return func(ctx *gin.Context) {
		request, err := NewAuthenticatedRequest(ctx)
		if err != nil {
			writeHandlerError(ctx, err, options)
			return
		}
		response, err := handler(request)
		if err != nil {
			writeHandlerError(ctx, err, options)
			return
		}
		writeResponse(ctx, response)
	}
}

// HandleWorkspaceScoped adapts a workspace-scoped typed handler to gin.HandlerFunc.
func HandleWorkspaceScoped(handler WorkspaceScopedHandler, opts ...HandlerOption) gin.HandlerFunc {
	options := newHandlerOptions(opts)
	return func(ctx *gin.Context) {
		request, err := NewWorkspaceScopedRequest(ctx)
		if err != nil {
			writeHandlerError(ctx, err, options)
			return
		}
		response, err := handler(request)
		if err != nil {
			writeHandlerError(ctx, err, options)
			return
		}
		writeResponse(ctx, response)
	}
}

// HandleAgentScoped adapts an Agent-scoped typed handler to gin.HandlerFunc.
func HandleAgentScoped(handler AgentScopedHandler, opts ...HandlerOption) gin.HandlerFunc {
	options := newHandlerOptions(opts)
	return func(ctx *gin.Context) {
		request, err := NewAgentScopedRequest(ctx)
		if err != nil {
			writeHandlerError(ctx, err, options)
			return
		}
		response, err := handler(request)
		if err != nil {
			writeHandlerError(ctx, err, options)
			return
		}
		if options.agentSuccess != nil {
			if err := options.agentSuccess(request, response); err != nil {
				writeHandlerError(ctx, err, options)
				return
			}
		}
		writeResponse(ctx, response)
	}
}

// BindJSON decodes a typed JSON request body using Gin's existing binder.
func BindJSON[T any](ctx *gin.Context) (T, error) {
	var request T
	if err := ctx.ShouldBindJSON(&request); err != nil {
		return request, err
	}
	return request, nil
}

// BindJSONStrict decodes a typed JSON request body and rejects unknown fields
// (DisallowUnknownFields), surfacing them as HTTP 400. Use it for canonical
// contracts that must not silently accept legacy input.
func BindJSONStrict[T any](ctx *gin.Context) (T, error) {
	var request T
	if ctx == nil || ctx.Request == nil || ctx.Request.Body == nil {
		return request, errors.New("request body is required")
	}
	dec := json.NewDecoder(ctx.Request.Body)
	dec.DisallowUnknownFields()
	if err := dec.Decode(&request); err != nil {
		return request, err
	}
	return request, nil
}

// BindEmptyOrJSON decodes a typed JSON request body where an absent, empty, or
// whitespace-only body is valid and returns the zero-value request with
// hasBody=false. A syntactically present body must be valid JSON.
//
// This is a low-level binder utility for domain-local legacy optional-body
// adapters. Required JSON handlers should continue to use BindJSON via
// HandleAuthJSON / HandleWorkspaceScopedJSON.
func BindEmptyOrJSON[T any](ctx *gin.Context) (request T, hasBody bool, err error) {
	if ctx == nil || ctx.Request == nil || ctx.Request.Body == nil {
		return request, false, nil
	}
	body, err := io.ReadAll(ctx.Request.Body)
	if err != nil {
		return request, false, err
	}
	if len(bytes.TrimSpace(body)) == 0 {
		return request, false, nil
	}
	if err := binding.JSON.BindBody(body, &request); err != nil {
		return request, true, err
	}
	return request, true, nil
}

// HandleAuthJSON adapts an auth-scoped JSON handler to gin.HandlerFunc.
func HandleAuthJSON[T any](handler func(AuthenticatedJSON[T]) (Response, error), opts ...HandlerOption) gin.HandlerFunc {
	options := newHandlerOptions(opts)
	return HandleAuth(func(request AuthenticatedRequest) (Response, error) {
		body, err := BindJSON[T](request.Gin)
		if err != nil {
			return Response{}, invalidJSONHTTPError(err, options.invalidJSONMessage)
		}
		return handler(AuthenticatedJSON[T]{AuthenticatedRequest: request, Body: body})
	}, opts...)
}

// HandleWorkspaceScopedJSON adapts a workspace-scoped JSON handler to gin.HandlerFunc.
func HandleWorkspaceScopedJSON[T any](handler func(WorkspaceScopedJSON[T]) (Response, error), opts ...HandlerOption) gin.HandlerFunc {
	options := newHandlerOptions(opts)
	return HandleWorkspaceScoped(func(request WorkspaceScopedRequest) (Response, error) {
		var body T
		var err error
		if options.strict {
			body, err = BindJSONStrict[T](request.Gin)
		} else {
			body, err = BindJSON[T](request.Gin)
		}
		if err != nil {
			return Response{}, invalidJSONHTTPError(err, options.invalidJSONMessage)
		}
		return handler(WorkspaceScopedJSON[T]{WorkspaceScopedRequest: request, Body: body})
	}, opts...)
}

// HandleAgentScopedJSON adapts an Agent-scoped JSON handler to gin.HandlerFunc.
func HandleAgentScopedJSON[T any](handler func(AgentScopedJSON[T]) (Response, error), opts ...HandlerOption) gin.HandlerFunc {
	options := newHandlerOptions(opts)
	return HandleAgentScoped(func(request AgentScopedRequest) (Response, error) {
		var body T
		var err error
		if options.strict {
			body, err = BindJSONStrict[T](request.Gin)
		} else {
			body, err = BindJSON[T](request.Gin)
		}
		if err != nil {
			return Response{}, invalidJSONHTTPError(err, options.invalidJSONMessage)
		}
		return handler(AgentScopedJSON[T]{AgentScopedRequest: request, Body: body})
	}, opts...)
}

func invalidJSONHTTPError(err error, message string) error {
	var maxBytesError *http.MaxBytesError
	if errors.As(err, &maxBytesError) {
		return NewHTTPErrorWithCause(http.StatusRequestEntityTooLarge, message, err)
	}
	return NewHTTPErrorWithCause(http.StatusBadRequest, message, err)
}

func writeResponse(ctx *gin.Context, response Response) {
	status := response.Status
	if status == 0 {
		status = http.StatusOK
	}
	for name, values := range response.Headers {
		for _, value := range values {
			ctx.Writer.Header().Add(name, value)
		}
	}
	if response.NoBody {
		ctx.Status(status)
		return
	}
	if response.Content != nil {
		if response.ContentType != "" {
			ctx.Header("Content-Type", response.ContentType)
		}
		http.ServeContent(ctx.Writer, ctx.Request, response.ContentName, response.ModifiedAt, response.Content)
		return
	}
	ctx.JSON(status, response.Body)
}

func writeHandlerError(ctx *gin.Context, err error, options handlerOptions) {
	var httpErr HTTPError
	if errors.As(err, &httpErr) {
		writeErrorResponse(ctx, err, ErrorResponse{Status: httpErr.Status, Message: httpErr.Error(), Code: httpErr.Code})
		return
	}
	for _, classifier := range options.classifiers {
		if response, ok := classifier(err); ok {
			writeErrorResponse(ctx, err, response)
			return
		}
	}
	StoreError(ctx, err)
}

func writeErrorResponse(ctx *gin.Context, err error, response ErrorResponse) {
	status := response.Status
	if status == 0 {
		status = http.StatusInternalServerError
	}
	observeServerError(ctx, err, status)
	if strings.TrimSpace(response.Code) != "" {
		ErrorWithContract(ctx, status, response.Message, response.Code, response.Details, ErrorContract{
			Stage: response.Stage, Path: response.Path, Hint: response.Hint,
			Retryable: response.Retryable, Diagnostics: response.Diagnostics,
			Truncation: response.Truncation,
		})
		return
	}
	Error(ctx, status, response.Message)
}
