package apperror

import (
	"errors"
	"fmt"
	"maps"
	"net/http"
	"time"
)

type Code string

// #nosec G101 -- these are client-visible error codes, not credentials
const (
	CodeInternal                        Code = "internal_error"
	CodeValidationFailed                Code = "validation_failed"
	CodeInvalidRequestBody              Code = "invalid_request_body"
	CodeRequestTimeout                  Code = "request_timeout"
	CodeNotFound                        Code = "not_found"
	CodeAlreadyInUse                    Code = "already_in_use"
	CodeForbidden                       Code = "forbidden"
	CodeInvalidToken                    Code = "invalid_token"
	CodeRateLimited                     Code = "rate_limited"
	CodeFileTooLarge                    Code = "file_too_large"
	CodeInvalidImage                    Code = "invalid_image"
	CodeInvalidWebAuthnResponse         Code = "invalid_webauthn_response"
	CodeWebAuthnAuthenticationFailed    Code = "webauthn_authentication_failed"
	CodePasskeyUserVerificationRequired Code = "passkey_user_verification_required"
	CodeSyncedPasskeyNotAllowed         Code = "synced_passkey_not_allowed"
	CodeInvalidWebAuthnSession          Code = "invalid_webauthn_session"
	CodeUserNotFound                    Code = "user_not_found"
	CodeUserDisabled                    Code = "user_disabled"
	CodeAPIKeyNotFound                  Code = "api_key_not_found"
	CodeInvalidAPIKey                   Code = "invalid_api_key"
	CodeDeviceLoginExpired              Code = "device_login_expired"
	CodeReauthenticationRequired        Code = "reauthentication_required"
	CodeEmailVerificationTokenInvalid   Code = "invalid_email_verification_token"
	CodeSetupNotAvailable               Code = "setup_not_available"
	CodeSetupAlreadyCompleted           Code = "setup_already_completed"
	CodeTokenInvalidOrExpired           Code = "token_invalid_or_expired"
	CodeDeviceCodeInvalid               Code = "device_code_invalid"
	CodeOidcMissingAuthorization        Code = "oidc_missing_authorization"
	CodeOidcInvalidCallbackURL          Code = "oidc_invalid_callback_url"
	CodeFileTypeNotSupported            Code = "file_type_not_supported"
	CodeNotSignedIn                     Code = "not_signed_in"
	CodeMissingSessionID                Code = "missing_session_id"
	CodeReservedClaim                   Code = "reserved_claim"
	CodeDuplicateClaim                  Code = "duplicate_claim"
	CodeLdapDisabled                    Code = "ldap_disabled"
	CodeLdapUserUpdate                  Code = "ldap_user_update"
	CodeLdapUserGroupUpdate             Code = "ldap_user_group_update"
	CodeOidcAccessDenied                Code = "oidc_access_denied"
	CodeOidcClientIDNotMatching         Code = "oidc_client_id_not_matching"
	CodeUIConfigDisabled                Code = "ui_config_disabled"
	CodeOneTimeAccessDisabled           Code = "one_time_access_disabled"
	CodeDeviceLoginDenied               Code = "device_login_denied"
	CodeNoAPIKeyProvided                Code = "no_api_key_provided"
	CodeAPIKeyNotExpired                Code = "api_key_not_expired"
	CodeInvalidAPIKeyExpiration         Code = "invalid_api_key_expiration"
	CodeAPIKeyAuthNotAllowed            Code = "api_key_auth_not_allowed"
	CodeOidcDeviceCodeExpired           Code = "oidc_device_code_expired"
	CodeOidcInvalidDeviceCode           Code = "oidc_invalid_device_code"
	CodeOpenSignupDisabled              Code = "open_signup_disabled"
	CodeClientIDAlreadyExists           Code = "client_id_already_exists"
	CodeUserEmailNotSet                 Code = "user_email_not_set"
	CodeImageNotFound                   Code = "image_not_found"
	CodeLogoDownloadFailed              Code = "logo_download_failed"
	CodeLogoTypeNotSupported            Code = "logo_type_not_supported"
	CodeLogoTooLarge                    Code = "logo_too_large"
	CodeOidcPARRequired                 Code = "oidc_par_required"
)

// FieldError describes one safe, client-actionable validation failure
type FieldError struct {
	Field   string `json:"field"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

// Error is an application error with a stable code, an HTTP status, and an optional internal cause
type Error struct {
	code       Code
	status     int
	message    string
	details    map[string]string
	fields     []FieldError
	retryAfter time.Duration
	cause      error
}

// New creates a client-safe application error
func New(code Code, status int, message string) *Error {
	return &Error{code: code, status: status, message: message}
}

// Wrap creates a client-safe application error that retains an internal cause
func Wrap(cause error, code Code, status int, message string) *Error {
	if cause == nil {
		return New(code, status, message)
	}

	return &Error{code: code, status: status, message: message, cause: cause}
}

// Internal creates a generic server error that retains a diagnostic cause without exposing it to clients
func Internal(cause error) *Error {
	return Wrap(cause, CodeInternal, http.StatusInternalServerError, "Something went wrong")
}

// Validation creates a structured validation error
func Validation(fields []FieldError) *Error {
	return New(CodeValidationFailed, http.StatusBadRequest, "Request validation failed").WithFields(fields)
}

// Error returns diagnostic text including the internal cause when one exists
func (e *Error) Error() string {
	if e == nil {
		return "<nil>"
	}
	if e.cause == nil {
		return e.message
	}

	return fmt.Sprintf("%s: %v", e.message, e.cause)
}

// Unwrap exposes the internal cause to errors.Is and errors.As
func (e *Error) Unwrap() error {
	if e == nil {
		return nil
	}

	return e.cause
}

// Is matches application errors by stable code while ignoring message and detail differences
func (e *Error) Is(target error) bool {
	targetError, ok := target.(*Error)
	return ok && e != nil && targetError != nil && e.code == targetError.code
}

// IsCode reports whether an error or one of its wrapped causes has the given application code
func IsCode(err error, code Code) bool {
	return errors.Is(err, &Error{code: code})
}

// Code returns the stable application error code
func (e *Error) Code() Code {
	if e == nil {
		return ""
	}

	return e.code
}

// HTTPStatus returns the HTTP status associated with the error
func (e *Error) HTTPStatus() int {
	if e == nil {
		return http.StatusInternalServerError
	}

	return e.status
}

// ClientMessage returns the message that is safe to include in an HTTP response
func (e *Error) ClientMessage() string {
	if e == nil {
		return ""
	}

	return e.message
}

// Details returns a copy of the additional client-safe details
func (e *Error) Details() map[string]string {
	if e == nil || len(e.details) == 0 {
		return nil
	}

	details := make(map[string]string, len(e.details))
	maps.Copy(details, e.details)

	return details
}

// Fields returns a copy of the structured validation details
func (e *Error) Fields() []FieldError {
	if e == nil || len(e.fields) == 0 {
		return nil
	}

	return append([]FieldError(nil), e.fields...)
}

// RetryAfter returns the duration a client should wait before retrying
func (e *Error) RetryAfter() time.Duration {
	if e == nil {
		return 0
	}

	return e.retryAfter
}

// WithFields attaches structured validation details without exposing the cause
func (e *Error) WithFields(fields []FieldError) *Error {
	if e == nil {
		return nil
	}

	e.fields = append([]FieldError(nil), fields...)
	return e
}

// WithDetail attaches one client-safe string detail
func (e *Error) WithDetail(key, value string) *Error {
	if e == nil {
		return nil
	}

	if e.details == nil {
		e.details = make(map[string]string)
	}
	e.details[key] = value
	return e
}

// WithRetryAfter attaches a retry delay to the error
func (e *Error) WithRetryAfter(retryAfter time.Duration) *Error {
	if e == nil {
		return nil
	}

	e.retryAfter = retryAfter
	return e
}
