// Package api contains shared helpers for API domain handlers.
package api

import (
	"errors"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
	"github.com/tidbcloud/AgentTeam/internal/workflow/publicerr"
	"github.com/tidbcloud/AgentTeam/internal/workspace/owner"
)

const (
	// WorkspaceOwnerMismatchCode is returned when a request reaches a server
	// profile that does not own the requested workspace.
	WorkspaceOwnerMismatchCode    = "workspace_owner_mismatch"
	WorkspaceOwnerMismatchMessage = "Workspace owner mismatch"
)

// RequireWorkspaceOwner converts workspace ownership failures to the shared API
// error shape so non-standard auth paths can keep the same 421/code contract.
func RequireWorkspaceOwner(managed owner.ManagedWorkspaceSet, workspaceID string) error {
	if err := owner.Require(managed, workspaceID); err != nil {
		return NewHTTPErrorWithCodeAndCause(
			http.StatusMisdirectedRequest,
			WorkspaceOwnerMismatchMessage,
			WorkspaceOwnerMismatchCode,
			err,
		)
	}
	return nil
}

// ErrorWorkspaceOwnerMismatch writes the shared owner-mismatch API response.
func ErrorWorkspaceOwnerMismatch(ctx *gin.Context) {
	ErrorWithCode(ctx, http.StatusMisdirectedRequest, WorkspaceOwnerMismatchMessage, WorkspaceOwnerMismatchCode)
}

// EnsureWorkspaceOwner writes the shared owner-mismatch response and returns
// whether the current server profile owns the workspace.
func EnsureWorkspaceOwner(ctx *gin.Context, managed owner.ManagedWorkspaceSet, workspaceID string) bool {
	if err := RequireWorkspaceOwner(managed, workspaceID); err != nil {
		ErrorWorkspaceOwnerMismatch(ctx)
		return false
	}
	return true
}

// Error writes a normalized JSON error payload.
func Error(ctx *gin.Context, status int, message string) {
	trimmed := strings.TrimSpace(message)
	if trimmed == "" {
		trimmed = http.StatusText(status)
		if trimmed == "" {
			trimmed = "Request failed"
		}
	}
	ctx.JSON(status, gin.H{"error": trimmed})
}

// ErrorWithCode writes a normalized JSON error payload with a stable code.
func ErrorWithCode(ctx *gin.Context, status int, message string, code string) {
	ErrorWithCodeAndDetails(ctx, status, message, code, nil)
}

// ErrorWithCodeAndDetails writes a normalized JSON error payload with a stable
// code and optional structured details.
func ErrorWithCodeAndDetails(ctx *gin.Context, status int, message string, code string, details any) {
	trimmedCode := strings.TrimSpace(code)
	if trimmedCode == "" {
		Error(ctx, status, message)
		return
	}

	trimmedMessage := strings.TrimSpace(message)
	if trimmedMessage == "" {
		trimmedMessage = http.StatusText(status)
		if trimmedMessage == "" {
			trimmedMessage = "Request failed"
		}
	}
	payload := gin.H{
		"error": trimmedMessage,
		"code":  trimmedCode,
	}
	if details != nil {
		payload["details"] = details
	}
	ctx.JSON(status, payload)
}

// ErrorContract carries the canonical public Workflow error contract fields
// beyond the generic {error, code, details} envelope. Omitted fields are
// dropped from the response so unrelated domains keep their existing shape.
type ErrorContract struct {
	Stage       string
	Path        string
	Hint        string
	Retryable   bool
	Diagnostics any
	Truncation  any
}

// ErrorWithContract writes a normalized JSON error payload with a stable code,
// optional structured details, and the canonical public Workflow contract
// fields. It is the single writer used by workflow domain classifiers so every
// public and Agent action projects the same envelope. The complete serialized
// envelope is budgeted below the desktop apiclient read ceiling so an oversized
// body never causes the apiclient to fail closed on a truncated response.
func ErrorWithContract(ctx *gin.Context, status int, message string, code string, details any, contract ErrorContract) {
	trimmedCode := strings.TrimSpace(code)
	if trimmedCode == "" {
		Error(ctx, status, message)
		return
	}
	trimmedMessage := strings.TrimSpace(message)
	if trimmedMessage == "" {
		trimmedMessage = http.StatusText(status)
		if trimmedMessage == "" {
			trimmedMessage = "Request failed"
		}
	}
	payload := gin.H{
		"error": trimmedMessage,
		"code":  trimmedCode,
	}
	if strings.TrimSpace(contract.Stage) != "" {
		payload["stage"] = contract.Stage
	}
	if strings.TrimSpace(contract.Path) != "" {
		payload["path"] = contract.Path
	}
	if strings.TrimSpace(contract.Hint) != "" {
		payload["hint"] = contract.Hint
	}
	if contract.Retryable {
		payload["retryable"] = true
	}
	if contract.Diagnostics != nil {
		payload["diagnostics"] = contract.Diagnostics
	}
	if contract.Truncation != nil {
		payload["truncation"] = contract.Truncation
	}
	if details != nil {
		payload["details"] = details
	}
	bounded, envelopeTruncation := publicerr.BudgetErrorEnvelope(payload, publicerr.EnvelopeBudgetOptions{
		DefaultProblem: trimmedMessage,
	})
	if envelopeTruncation != nil {
		// Preserve an existing diagnostics-level truncation report (from
		// classification) and merge the whole-envelope cut into it, so the
		// caller sees one deterministic truncation payload.
		payload = bounded
	}
	ctx.JSON(status, payload)
}

// StatusForError maps domain/store errors to the legacy browser API status code.
func StatusForError(err error) int {
	var quotaErr plan.QuotaExceededError
	switch {
	case errors.As(err, &quotaErr):
		return http.StatusForbidden
	case errors.Is(err, ErrUnauthorized):
		return http.StatusUnauthorized
	case errors.Is(err, ErrForbidden):
		return http.StatusForbidden
	case errors.Is(err, ErrNotFound):
		return http.StatusNotFound
	case errors.Is(err, ErrConflict):
		return http.StatusConflict
	case errors.Is(err, ErrGone):
		return http.StatusGone
	case errors.Is(err, ErrSystemAlreadyInitialized):
		return http.StatusConflict
	case errors.Is(err, ErrSystemNotInitialized):
		return http.StatusConflict
	case errors.Is(err, ErrBadRequest):
		return http.StatusBadRequest
	case errors.Is(err, message.ErrForbidden):
		return http.StatusForbidden
	case errors.Is(err, message.ErrNotFound):
		return http.StatusNotFound
	case errors.Is(err, message.ErrConflict):
		return http.StatusConflict
	case errors.Is(err, message.ErrContentModerationBlocked):
		return http.StatusBadRequest
	case errors.Is(err, message.ErrMessageSearchUnavailable):
		return http.StatusNotImplemented
	case errors.Is(err, taskboard.ErrForbidden):
		return http.StatusForbidden
	case errors.Is(err, taskboard.ErrNotFound):
		return http.StatusNotFound
	case errors.Is(err, taskboard.ErrConflict):
		return http.StatusConflict
	case errors.Is(err, taskboard.ErrBadRequest):
		return http.StatusBadRequest
	case errors.Is(err, domain.ErrInvalidArgument):
		return http.StatusBadRequest
	case errors.Is(err, owner.ErrOwnerMismatch):
		return http.StatusMisdirectedRequest
	default:
		return http.StatusInternalServerError
	}
}

// StoreError writes err using StatusForError and the legacy browser API envelope.
func StoreError(ctx *gin.Context, err error) {
	status := StatusForError(err)
	observeServerError(ctx, err, status)

	var quotaErr plan.QuotaExceededError
	if errors.As(err, &quotaErr) {
		ctx.JSON(status, gin.H{
			"error":          quotaErr.Error(),
			"code":           quotaErr.Code(),
			"metric":         quotaErr.Metric,
			"limit":          quotaErr.Limit,
			"current":        quotaErr.Current,
			"requestedDelta": quotaErr.Delta,
			"plan":           quotaErr.PlanKey,
			"planName":       quotaErr.PlanName,
			"quotaScope":     quotaErr.Scope,
			"organizationId": quotaErr.OrganizationID,
		})
		return
	}
	if errors.Is(err, message.ErrContentModerationBlocked) {
		ErrorWithCode(ctx, status, "content contains disallowed information", message.ContentModerationBlockedCode)
		return
	}
	var transitionErr taskboard.InvalidTaskStatusTransitionError
	if errors.As(err, &transitionErr) {
		ErrorWithCodeAndDetails(ctx, status, transitionErr.Error(), taskboard.InvalidTaskStatusTransitionCode, transitionErr.Details())
		return
	}
	Error(ctx, status, err.Error())
}
