package server

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/mem9-ai/drive9/pkg/meta"
	"github.com/mem9-ai/drive9/pkg/tenant"
)

// This file holds the control-plane error envelope shared by every
// control-plane API (provision, quota, admin tenants / tenant-pool /
// object-backends / extract-config / embedding-config, tokens): the provision
// error type, the stable machine-readable code mapping, the free-tier
// guidance messages, and the response writers. Keep it out of server.go so
// the error surface stays a self-contained package.

// provisionTenantError carries the wire fields for a failed /v1/provision
// attempt. code is optional: when empty it is derived at write time from the
// wrapped error (sentinel mapping) or, failing that, from a generic status
// fallback. Sites whose failure has an operation-specific meaning (e.g. tenant
// pool claim) set code explicitly.
type provisionTenantError struct {
	status  int
	message string
	code    string
	details map[string]any
	err     error
}

func (e *provisionTenantError) Error() string {
	if e.err != nil {
		return e.err.Error()
	}
	return e.message
}

func (e *provisionTenantError) Unwrap() error { return e.err }

func newProvisionTenantError(status int, message string, err error) *provisionTenantError {
	return &provisionTenantError{status: status, message: message, err: err}
}

// controlPlaneActionAddPaymentMethod directs the client to have the user bind
// a payment method (upgrade the organization). The concrete billing URL is a
// client-side concern and intentionally not emitted by the server.
const controlPlaneActionAddPaymentMethod = "add_payment_method"

// controlPlaneErrorAction is the machine-readable guidance attached to
// control-plane error responses so clients can route users to the right fix
// (e.g. bind a payment method to lift free-tier limits) without string
// matching. The client owns the concrete remediation URL for an action type.
type controlPlaneErrorAction struct {
	Type string `json:"type"`
}

// controlPlaneActionForCode returns the recommended remediation for a
// control-plane error code, or nil when no action is needed (retry,
// permission, backend).
func controlPlaneActionForCode(code string) *controlPlaneErrorAction {
	switch code {
	case "free_tenant_limit_reached", "free_quota_exceeded", "free_spending_limit_forbidden",
		"free_quota_mutation_forbidden", "free_tenant_pool_forbidden":
		return &controlPlaneErrorAction{Type: controlPlaneActionAddPaymentMethod}
	}
	return nil
}

// controlPlaneCodeFromErr returns the stable machine-readable code for a
// control-plane failure via sentinel mapping only (no status inference), or
// "" when err carries no mapped sentinel. Codes are condition-first and
// prefix-free so one code means one condition on every control-plane API.
func controlPlaneCodeFromErr(err error) string {
	switch {
	case errors.Is(err, tenant.ErrTiDBCloudFreeTenantLimitReached):
		return "free_tenant_limit_reached"
	case errors.Is(err, tenant.ErrTiDBCloudFreeQuotaBusy):
		return "free_quota_busy"
	case errors.Is(err, tenant.ErrTiDBCloudFreeQuotaExceeded):
		return "free_quota_exceeded"
	case errors.Is(err, tenant.ErrTiDBCloudFreeSpendingLimitForbidden):
		return "free_spending_limit_forbidden"
	case errors.Is(err, tenant.ErrTiDBCloudFreeQuotaMutationForbidden):
		return "free_quota_mutation_forbidden"
	case errors.Is(err, tenant.ErrTiDBCloudFreeTenantPoolForbidden):
		return "free_tenant_pool_forbidden"
	case errors.Is(err, errTiDBCloudSpendingLimitInvalid):
		return "spending_limit_invalid"
	case errors.Is(err, tenant.ErrQuotaPermissionDenied), errors.Is(err, tenant.ErrTiDBCloudRoleInsufficient):
		return "quota_permission_denied"
	case errors.Is(err, tenant.ErrQuotaBackendNotFound):
		return "quota_backend_not_found"
	case errors.Is(err, meta.ErrTenantExtractConfigNotEnabled):
		return "extract_config_not_enabled"
	case errors.Is(err, tenant.ErrCredentialsRequired), errors.Is(err, tenant.ErrPartialCredentials):
		return "credentials_required"
	case errors.Is(err, tenant.ErrTiDBCloudBillingUnavailable), errors.Is(err, tenant.ErrTiDBCloudBillingResponseInvalid):
		return "billing_unavailable"
	}
	return ""
}

// controlPlaneErrorCode maps a control-plane failure to a stable
// machine-readable code. The mapping is sentinel-driven; the status fallback
// is deliberately generic (no operation names) so a code can never mislabel a
// failure path — operation-specific codes (e.g. tenant_pool_claim_failed) are
// set explicitly at their construction sites instead.
func controlPlaneErrorCode(status int, err error) string {
	if code := controlPlaneCodeFromErr(err); code != "" {
		return code
	}
	switch status {
	case http.StatusUnauthorized:
		return "unauthorized"
	case http.StatusForbidden:
		return "forbidden"
	case http.StatusPaymentRequired:
		return "payment_required"
	case http.StatusNotFound:
		return "not_found"
	case http.StatusMethodNotAllowed:
		return "method_not_allowed"
	case http.StatusBadRequest:
		return "invalid_request"
	case http.StatusConflict:
		return "conflict"
	case http.StatusBadGateway:
		return "upstream_failed"
	case http.StatusGatewayTimeout:
		return "upstream_timeout"
	case http.StatusServiceUnavailable:
		return "unavailable"
	default:
		return "failed"
	}
}

// newPoolClaimError wraps a tenant-pool claim failure. The claim-specific
// code (tenant_pool_claim_failed) applies only when the failure carries no
// more specific sentinel (free-tier limit, billing, permission) — those keep
// their sentinel code and its recommended action.
func newPoolClaimError(err error) *provisionTenantError {
	status, msg := clientFacingErrorResponse(http.StatusBadGateway, "claim tenant pool tenant failed", err)
	pe := newProvisionTenantError(status, msg, err)
	if controlPlaneCodeFromErr(err) == "" {
		pe.code = "tenant_pool_claim_failed"
	}
	return pe
}

// writeControlPlaneError writes the control-plane error envelope with an
// explicit code. The recommended action (if any) is derived from the code, so
// free-tier codes automatically carry add_payment_method guidance.
func writeControlPlaneError(w http.ResponseWriter, status int, code, msg string) {
	writeControlPlaneErrorBody(w, status, msg, code, nil)
}

// writeControlPlaneStatusError writes the control-plane error envelope using
// the generic status-derived code (invalid_request / not_found / conflict /
// forbidden / ...). Use for validation and configuration rejections that do
// not carry a more specific semantic code.
func writeControlPlaneStatusError(w http.ResponseWriter, status int, msg string) {
	writeControlPlaneError(w, status, controlPlaneErrorCode(status, nil), msg)
}

// writeControlPlaneErrorBody writes the control-plane error envelope. The
// "error" field stays a plain string (message) so existing clients that read
// it keep working; "code"/"details"/"action" are additive machine-readable
// fields.
func writeControlPlaneErrorBody(w http.ResponseWriter, status int, msg, code string, details map[string]any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	body := map[string]any{
		"error": msg,
		"code":  code,
	}
	if len(details) > 0 {
		body["details"] = details
	}
	if action := controlPlaneActionForCode(code); action != nil {
		body["action"] = action
	}
	_ = json.NewEncoder(w).Encode(body)
}

// writeProvisionTenantError writes a provision failure with the structured
// envelope: a stable machine-readable "code", optional "details", and an
// optional "action" (e.g. add_payment_method) guiding the user to the fix.
// The "error" field keeps the human-readable message, preserving the existing
// flat envelope for older clients.
func writeProvisionTenantError(w http.ResponseWriter, err *provisionTenantError) {
	if errors.Is(err, tenant.ErrTiDBCloudFreeQuotaBusy) {
		w.Header().Set("Retry-After", "1")
	}
	code := err.code
	if code == "" {
		code = controlPlaneErrorCode(err.status, err.err)
	}
	writeControlPlaneErrorBody(w, err.status, err.message, code, err.details)
}

// Free-tier guidance messages. Each keeps its sentinel phrase as a prefix so
// existing string-based matching (logs, e2e greps, older clients) still works,
// while humans get an actionable hint: free-tier limits are lifted by binding
// a payment method / upgrading the organization.

const freeTenantLimitReachedGuidance = "; add a payment method or upgrade to create more tenants"

func freeTenantLimitReachedMessage() string {
	return tenant.ErrTiDBCloudFreeTenantLimitReached.Error() + freeTenantLimitReachedGuidance
}

// freeTenantLimitReachedUsageMessage is the variant used when the current
// usage is known (persist path): it states how many of the allowed free
// tenants are already used.
func freeTenantLimitReachedUsageMessage(count, limit int) string {
	return fmt.Sprintf("%s: %d of %d free tenants used%s",
		tenant.ErrTiDBCloudFreeTenantLimitReached.Error(), count, limit, freeTenantLimitReachedGuidance)
}

func freeQuotaExceededMessage() string {
	return tenant.ErrTiDBCloudFreeQuotaExceeded.Error() + "; add a payment method or upgrade for larger limits"
}

func freeSpendingLimitForbiddenMessage() string {
	return tenant.ErrTiDBCloudFreeSpendingLimitForbidden.Error() + "; add a payment method or upgrade to set spending limits"
}

func freeQuotaMutationForbiddenMessage() string {
	return tenant.ErrTiDBCloudFreeQuotaMutationForbidden.Error() + "; add a payment method or upgrade to change quota"
}

func freeTenantPoolForbiddenMessage() string {
	return tenant.ErrTiDBCloudFreeTenantPoolForbidden.Error() + "; add a payment method or upgrade to create tenant pools"
}

// freeProvisionQuotaError builds the 402 Payment Required error for a free
// plan quota rejection: the requested quota exceeds what the free plan allows,
// and the org must upgrade (bind a payment method) to request larger limits.
// Details carry the free plan caps so clients can show exactly what is allowed.
func (s *Server) freeProvisionQuotaError(err error) *provisionTenantError {
	limits := normalizeTiDBCloudFreePlanLimits(s.tidbCloudFreePlanLimits)
	msg := freeQuotaExceededMessage()
	// Spending-limit rejections are not explained by the plan caps, so they
	// carry no details; quota-above-caps rejections show the allowed caps.
	if errors.Is(err, tenant.ErrTiDBCloudFreeSpendingLimitForbidden) {
		return &provisionTenantError{
			status:  http.StatusPaymentRequired,
			message: freeSpendingLimitForbiddenMessage(),
			err:     err,
		}
	}
	return &provisionTenantError{
		status:  http.StatusPaymentRequired,
		message: msg,
		details: map[string]any{
			"max_storage_bytes":   limits.MaxStorageBytes,
			"max_file_size_bytes": limits.MaxFileSizeBytes,
			"max_file_count":      limits.MaxFileCount,
		},
		err: err,
	}
}
