// Package httperror provides typed HTTP errors with sentinel error matching.
//
// NOTE: This file mirrors github.com/orchard9/rdev/pkg/api/error.go
// When updating patterns here, consider updating the rdev source as well.
//
// HTTPError implements the error interface and provides errors.Is() matching
// for idiomatic Go error handling in HTTP handlers.
//
// Usage:
//
//	func GetUser(w http.ResponseWriter, r *http.Request) error {
//	    user, err := svc.Get(ctx, id)
//	    if err != nil {
//	        if errors.Is(err, ErrUserNotFound) {
//	            return httperror.NotFoundf("user %s not found", id)
//	        }
//	        return err
//	    }
//	    httpresponse.OK(w, r, user)
//	    return nil
//	}
//
//	// In error handling middleware, check error types:
//	if errors.Is(err, httperror.ErrNotFound) {
//	    // handle not found
//	}
package httperror

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

// HTTPError is a typed error that maps to an HTTP response.
// It implements the error interface and provides sentinel error matching via errors.Is().
type HTTPError struct {
	Status  int    // HTTP status code
	Code    string // Machine-readable error code
	Message string // Human-readable message
	Details any    // Optional additional details
	cause   error  // Underlying error for wrapping
}

// Error implements the error interface.
func (e *HTTPError) Error() string {
	if e.cause != nil {
		return fmt.Sprintf("%s: %v", e.Message, e.cause)
	}
	return e.Message
}

// Unwrap returns the underlying error for errors.Unwrap().
func (e *HTTPError) Unwrap() error {
	return e.cause
}

// Is implements errors.Is() matching for sentinel errors.
// Matches if both errors are HTTPErrors with the same status code.
func (e *HTTPError) Is(target error) bool {
	var t *HTTPError
	if errors.As(target, &t) {
		return e.Status == t.Status
	}
	return false
}

// WithCause returns a copy of the error with the given underlying cause.
func (e *HTTPError) WithCause(cause error) *HTTPError {
	return &HTTPError{
		Status:  e.Status,
		Code:    e.Code,
		Message: e.Message,
		Details: e.Details,
		cause:   cause,
	}
}

// -----------------------------------------------------------------------------
// Sentinel Errors
// -----------------------------------------------------------------------------

// Sentinel errors for errors.Is() matching.
// Use these with errors.Is() to check error types:
//
//	if errors.Is(err, httperror.ErrNotFound) {
//	    // handle not found
//	}
var (
	ErrBadRequest          = &HTTPError{Status: http.StatusBadRequest, Code: "BAD_REQUEST", Message: "bad request"}
	ErrUnauthorized        = &HTTPError{Status: http.StatusUnauthorized, Code: "UNAUTHORIZED", Message: "unauthorized"}
	ErrForbidden           = &HTTPError{Status: http.StatusForbidden, Code: "FORBIDDEN", Message: "forbidden"}
	ErrNotFound            = &HTTPError{Status: http.StatusNotFound, Code: "NOT_FOUND", Message: "not found"}
	ErrConflict            = &HTTPError{Status: http.StatusConflict, Code: "CONFLICT", Message: "conflict"}
	ErrUnprocessableEntity = &HTTPError{Status: http.StatusUnprocessableEntity, Code: "UNPROCESSABLE_ENTITY", Message: "unprocessable entity"}
	ErrTooManyRequests     = &HTTPError{Status: http.StatusTooManyRequests, Code: "TOO_MANY_REQUESTS", Message: "too many requests"}
	ErrInternal            = &HTTPError{Status: http.StatusInternalServerError, Code: "INTERNAL_ERROR", Message: "internal error"}
	ErrServiceUnavailable  = &HTTPError{Status: http.StatusServiceUnavailable, Code: "SERVICE_UNAVAILABLE", Message: "service unavailable"}
	ErrValidation          = &HTTPError{Status: http.StatusBadRequest, Code: "VALIDATION_ERROR", Message: "validation failed"}
)

// -----------------------------------------------------------------------------
// Factory Functions
// -----------------------------------------------------------------------------

// BadRequest creates a 400 Bad Request error.
func BadRequest(msg string) error {
	return &HTTPError{
		Status:  http.StatusBadRequest,
		Code:    "BAD_REQUEST",
		Message: msg,
	}
}

// BadRequestf creates a 400 Bad Request error with formatted message.
func BadRequestf(format string, args ...any) error {
	return BadRequest(fmt.Sprintf(format, args...))
}

// Unauthorized creates a 401 Unauthorized error.
func Unauthorized(msg string) error {
	return &HTTPError{
		Status:  http.StatusUnauthorized,
		Code:    "UNAUTHORIZED",
		Message: msg,
	}
}

// Unauthorizedf creates a 401 Unauthorized error with formatted message.
func Unauthorizedf(format string, args ...any) error {
	return Unauthorized(fmt.Sprintf(format, args...))
}

// Forbidden creates a 403 Forbidden error.
func Forbidden(msg string) error {
	return &HTTPError{
		Status:  http.StatusForbidden,
		Code:    "FORBIDDEN",
		Message: msg,
	}
}

// Forbiddenf creates a 403 Forbidden error with formatted message.
func Forbiddenf(format string, args ...any) error {
	return Forbidden(fmt.Sprintf(format, args...))
}

// NotFound creates a 404 Not Found error.
func NotFound(msg string) error {
	return &HTTPError{
		Status:  http.StatusNotFound,
		Code:    "NOT_FOUND",
		Message: msg,
	}
}

// NotFoundf creates a 404 Not Found error with formatted message.
func NotFoundf(format string, args ...any) error {
	return NotFound(fmt.Sprintf(format, args...))
}

// Conflict creates a 409 Conflict error.
func Conflict(msg string) error {
	return &HTTPError{
		Status:  http.StatusConflict,
		Code:    "CONFLICT",
		Message: msg,
	}
}

// Conflictf creates a 409 Conflict error with formatted message.
func Conflictf(format string, args ...any) error {
	return Conflict(fmt.Sprintf(format, args...))
}

// Internal creates a 500 Internal Server Error.
func Internal(msg string) error {
	return &HTTPError{
		Status:  http.StatusInternalServerError,
		Code:    "INTERNAL_ERROR",
		Message: msg,
	}
}

// Internalf creates a 500 Internal Server Error with formatted message.
func Internalf(format string, args ...any) error {
	return Internal(fmt.Sprintf(format, args...))
}

// Validation creates a 400 validation error.
func Validation(msg string) error {
	return &HTTPError{
		Status:  http.StatusBadRequest,
		Code:    "VALIDATION_ERROR",
		Message: msg,
	}
}

// Validationf creates a 400 validation error with formatted message.
func Validationf(format string, args ...any) error {
	return Validation(fmt.Sprintf(format, args...))
}

// UnprocessableEntity creates a 422 Unprocessable Entity error.
// Use for validation failures when the request is syntactically correct
// but semantically invalid.
func UnprocessableEntity(msg string) error {
	return &HTTPError{
		Status:  http.StatusUnprocessableEntity,
		Code:    "UNPROCESSABLE_ENTITY",
		Message: msg,
	}
}

// UnprocessableEntityf creates a 422 Unprocessable Entity error with formatted message.
func UnprocessableEntityf(format string, args ...any) error {
	return UnprocessableEntity(fmt.Sprintf(format, args...))
}

// TooManyRequests creates a 429 Too Many Requests error.
func TooManyRequests(msg string) error {
	return &HTTPError{
		Status:  http.StatusTooManyRequests,
		Code:    "TOO_MANY_REQUESTS",
		Message: msg,
	}
}

// TooManyRequestsf creates a 429 Too Many Requests error with formatted message.
func TooManyRequestsf(format string, args ...any) error {
	return TooManyRequests(fmt.Sprintf(format, args...))
}

// ServiceUnavailable creates a 503 Service Unavailable error.
func ServiceUnavailable(msg string) error {
	return &HTTPError{
		Status:  http.StatusServiceUnavailable,
		Code:    "SERVICE_UNAVAILABLE",
		Message: msg,
	}
}

// ServiceUnavailablef creates a 503 Service Unavailable error with formatted message.
func ServiceUnavailablef(format string, args ...any) error {
	return ServiceUnavailable(fmt.Sprintf(format, args...))
}

// -----------------------------------------------------------------------------
// Error Wrapping
// -----------------------------------------------------------------------------

// WithDetails returns an error with additional details attached.
// If the error is not an HTTPError, it wraps it as an internal error.
func WithDetails(err error, details any) error {
	var httpErr *HTTPError
	if errors.As(err, &httpErr) {
		return &HTTPError{
			Status:  httpErr.Status,
			Code:    httpErr.Code,
			Message: httpErr.Message,
			Details: details,
			cause:   httpErr.cause,
		}
	}
	// Not an HTTPError, wrap as internal error with details
	return &HTTPError{
		Status:  http.StatusInternalServerError,
		Code:    "INTERNAL_ERROR",
		Message: err.Error(),
		Details: details,
	}
}

// WithCode returns an error with a custom error code.
// Useful for domain-specific error codes like "KEY_REVOKED".
func WithCode(err error, code string) error {
	var httpErr *HTTPError
	if errors.As(err, &httpErr) {
		return &HTTPError{
			Status:  httpErr.Status,
			Code:    code,
			Message: httpErr.Message,
			Details: httpErr.Details,
			cause:   httpErr.cause,
		}
	}
	return &HTTPError{
		Status:  http.StatusInternalServerError,
		Code:    code,
		Message: err.Error(),
	}
}

// WrapError wraps an underlying error with an HTTPError.
// The underlying error is accessible via errors.Unwrap().
func WrapError(httpErr *HTTPError, cause error) error {
	return httpErr.WithCause(cause)
}

// -----------------------------------------------------------------------------
// Error Checking
// -----------------------------------------------------------------------------

// IsHTTPError checks if an error is an HTTPError.
func IsHTTPError(err error) bool {
	var httpErr *HTTPError
	return errors.As(err, &httpErr)
}

// AsHTTPError extracts an HTTPError from an error chain.
// Returns nil if the error is not an HTTPError.
func AsHTTPError(err error) *HTTPError {
	var httpErr *HTTPError
	if errors.As(err, &httpErr) {
		return httpErr
	}
	return nil
}

// StatusCode returns the HTTP status code for an error.
// Returns 200 for nil errors (success case).
// Returns 500 for non-HTTPErrors.
func StatusCode(err error) int {
	if err == nil {
		return http.StatusOK
	}
	if httpErr := AsHTTPError(err); httpErr != nil {
		return httpErr.Status
	}
	return http.StatusInternalServerError
}
