// Package httperror provides RFC 9457 compliant error handling for HTTP APIs.
// RFC 9457: Problem Details for HTTP APIs
// https://www.rfc-editor.org/rfc/rfc9457.html
package httperror

import (
	"encoding/json"
	"net/http"
	"time"
)

// ProblemDetail represents an RFC 9457 problem detail response.
// It provides a standardized way to carry machine-readable details of errors
// in HTTP response content.
type ProblemDetail struct {
	// Standard RFC 9457 fields

	// Type is a URI reference that identifies the problem type.
	// When dereferenced, it should provide human-readable documentation.
	// Defaults to "about:blank" if not provided.
	Type string `json:"type"`

	// Status is the HTTP status code for this occurrence of the problem.
	Status int `json:"status"`

	// Title is a short, human-readable summary of the problem type.
	Title string `json:"title"`

	// Detail is a human-readable explanation specific to this occurrence.
	Detail string `json:"detail,omitempty"`

	// Instance is a URI reference that identifies this specific occurrence.
	Instance string `json:"instance,omitempty"`

	// MapleFile-specific extensions

	// Errors contains field-specific validation errors.
	// Key is the field name, value is the error message.
	Errors map[string]string `json:"errors,omitempty"`

	// Timestamp is the ISO 8601 timestamp when the error occurred.
	Timestamp string `json:"timestamp"`

	// TraceID is the request trace ID for debugging.
	TraceID string `json:"trace_id,omitempty"`
}

// Problem type URIs - these identify categories of errors
const (
	TypeValidationError    = "https://api.maplefile.com/problems/validation-error"
	TypeBadRequest         = "https://api.maplefile.com/problems/bad-request"
	TypeUnauthorized       = "https://api.maplefile.com/problems/unauthorized"
	TypeForbidden          = "https://api.maplefile.com/problems/forbidden"
	TypeNotFound           = "https://api.maplefile.com/problems/not-found"
	TypeConflict           = "https://api.maplefile.com/problems/conflict"
	TypeTooManyRequests    = "https://api.maplefile.com/problems/too-many-requests"
	TypeInternalError      = "https://api.maplefile.com/problems/internal-error"
	TypeServiceUnavailable = "https://api.maplefile.com/problems/service-unavailable"
)

// NewProblemDetail creates a new RFC 9457 problem detail.
func NewProblemDetail(status int, problemType, title, detail string) *ProblemDetail {
	return &ProblemDetail{
		Type:      problemType,
		Status:    status,
		Title:     title,
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewValidationError creates a validation error problem detail.
// Use this when one or more fields fail validation.
func NewValidationError(fieldErrors map[string]string) *ProblemDetail {
	detail := "One or more fields failed validation. Please check the errors and try again."
	if len(fieldErrors) == 0 {
		detail = "Validation failed."
	}

	return &ProblemDetail{
		Type:      TypeValidationError,
		Status:    http.StatusBadRequest,
		Title:     "Validation Failed",
		Detail:    detail,
		Errors:    fieldErrors,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewBadRequestError creates a generic bad request error.
// Use this for malformed requests or invalid input.
func NewBadRequestError(detail string) *ProblemDetail {
	return &ProblemDetail{
		Type:      TypeBadRequest,
		Status:    http.StatusBadRequest,
		Title:     "Bad Request",
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewUnauthorizedError creates an unauthorized error.
// Use this when authentication is required but missing or invalid.
func NewUnauthorizedError(detail string) *ProblemDetail {
	if detail == "" {
		detail = "Authentication is required to access this resource."
	}

	return &ProblemDetail{
		Type:      TypeUnauthorized,
		Status:    http.StatusUnauthorized,
		Title:     "Unauthorized",
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewForbiddenError creates a forbidden error.
// Use this when the user is authenticated but lacks permission.
func NewForbiddenError(detail string) *ProblemDetail {
	if detail == "" {
		detail = "You do not have permission to access this resource."
	}

	return &ProblemDetail{
		Type:      TypeForbidden,
		Status:    http.StatusForbidden,
		Title:     "Forbidden",
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewNotFoundError creates a not found error.
// Use this when a requested resource does not exist.
func NewNotFoundError(resourceType string) *ProblemDetail {
	detail := "The requested resource was not found."
	if resourceType != "" {
		detail = resourceType + " not found."
	}

	return &ProblemDetail{
		Type:      TypeNotFound,
		Status:    http.StatusNotFound,
		Title:     "Not Found",
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewConflictError creates a conflict error.
// Use this when the request conflicts with the current state.
func NewConflictError(detail string) *ProblemDetail {
	if detail == "" {
		detail = "The request conflicts with the current state of the resource."
	}

	return &ProblemDetail{
		Type:      TypeConflict,
		Status:    http.StatusConflict,
		Title:     "Conflict",
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewTooManyRequestsError creates a rate limit exceeded error.
// Use this when the client has exceeded the allowed request rate.
// CWE-307: Used to prevent brute force attacks by limiting request frequency.
func NewTooManyRequestsError(detail string) *ProblemDetail {
	if detail == "" {
		detail = "Too many requests. Please try again later."
	}

	return &ProblemDetail{
		Type:      TypeTooManyRequests,
		Status:    http.StatusTooManyRequests,
		Title:     "Too Many Requests",
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewInternalServerError creates an internal server error.
// Use this for unexpected errors that are not the client's fault.
func NewInternalServerError(detail string) *ProblemDetail {
	if detail == "" {
		detail = "An unexpected error occurred. Please try again later."
	}

	return &ProblemDetail{
		Type:      TypeInternalError,
		Status:    http.StatusInternalServerError,
		Title:     "Internal Server Error",
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// NewServiceUnavailableError creates a service unavailable error.
// Use this when the service is temporarily unavailable.
func NewServiceUnavailableError(detail string) *ProblemDetail {
	if detail == "" {
		detail = "The service is temporarily unavailable. Please try again later."
	}

	return &ProblemDetail{
		Type:      TypeServiceUnavailable,
		Status:    http.StatusServiceUnavailable,
		Title:     "Service Unavailable",
		Detail:    detail,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}
}

// WithInstance adds the request path as the instance identifier.
func (p *ProblemDetail) WithInstance(instance string) *ProblemDetail {
	p.Instance = instance
	return p
}

// WithTraceID adds the request trace ID for debugging.
func (p *ProblemDetail) WithTraceID(traceID string) *ProblemDetail {
	p.TraceID = traceID
	return p
}

// WithError adds a single field error to the problem detail.
func (p *ProblemDetail) WithError(field, message string) *ProblemDetail {
	if p.Errors == nil {
		p.Errors = make(map[string]string)
	}
	p.Errors[field] = message
	return p
}

// Error implements the error interface.
func (p *ProblemDetail) Error() string {
	if p.Detail != "" {
		return p.Detail
	}
	return p.Title
}

// ExtractRequestID gets the request ID from the request context or headers.
// This uses the existing request ID middleware.
func ExtractRequestID(r *http.Request) string {
	// Try to get from context first (preferred)
	if requestID := r.Context().Value("request_id"); requestID != nil {
		if id, ok := requestID.(string); ok {
			return id
		}
	}

	// Fallback to header
	if requestID := r.Header.Get("X-Request-ID"); requestID != "" {
		return requestID
	}

	// No request ID found
	return ""
}

// RespondWithProblem writes the RFC 9457 problem detail to the HTTP response.
// It sets the appropriate Content-Type header and status code.
func RespondWithProblem(w http.ResponseWriter, problem *ProblemDetail) {
	w.Header().Set("Content-Type", "application/problem+json")
	w.WriteHeader(problem.Status)
	json.NewEncoder(w).Encode(problem)
}

// RespondWithError is a convenience function that handles both ProblemDetail
// and standard Go errors. If the error is a ProblemDetail, it writes it directly.
// Otherwise, it wraps it in an internal server error.
func RespondWithError(w http.ResponseWriter, r *http.Request, err error) {
	requestID := ExtractRequestID(r)

	// Check if error is already a ProblemDetail
	if problem, ok := err.(*ProblemDetail); ok {
		problem.WithInstance(r.URL.Path).WithTraceID(requestID)
		RespondWithProblem(w, problem)
		return
	}

	// Wrap standard error in internal server error
	problem := NewInternalServerError(err.Error())
	problem.WithInstance(r.URL.Path).WithTraceID(requestID)
	RespondWithProblem(w, problem)
}
