// Package responses provides structured API response types with security-conscious error handling.
package responses

import (
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"strings"
	"time"

	"github.com/jfraeys/fetch_ml/internal/logging"
)

// ErrorResponse provides a sanitized error response to clients.
// It includes a trace ID for support lookup while preventing information leakage.
type ErrorResponse struct {
	Error   string `json:"error"`    // Sanitized error message for clients
	Code    string `json:"code"`     // Machine-readable error code
	TraceID string `json:"trace_id"` // For support lookup (internal correlation)
}

// Error codes for machine-readable error identification
const (
	ErrCodeBadRequest         = "BAD_REQUEST"
	ErrCodeUnauthorized       = "UNAUTHORIZED"
	ErrCodeForbidden          = "FORBIDDEN"
	ErrCodeNotFound           = "NOT_FOUND"
	ErrCodeConflict           = "CONFLICT"
	ErrCodeRateLimited        = "RATE_LIMITED"
	ErrCodeInternal           = "INTERNAL_ERROR"
	ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
	ErrCodeValidation         = "VALIDATION_ERROR"
)

// HTTP status to error code mapping
var statusToCode = map[int]string{
	http.StatusBadRequest:          ErrCodeBadRequest,
	http.StatusUnauthorized:        ErrCodeUnauthorized,
	http.StatusForbidden:           ErrCodeForbidden,
	http.StatusNotFound:            ErrCodeNotFound,
	http.StatusConflict:            ErrCodeConflict,
	http.StatusTooManyRequests:     ErrCodeRateLimited,
	http.StatusInternalServerError: ErrCodeInternal,
	http.StatusServiceUnavailable:  ErrCodeServiceUnavailable,
	422:                            ErrCodeValidation, // Unprocessable Entity
}

// Patterns to sanitize from error messages (security: prevent information leakage)
var (
	// Remove file paths
	pathPattern = regexp.MustCompile(`/[^\s]*`)
	// Remove sensitive keywords
	sensitiveKeywords = []string{"password", "secret", "token", "key", "credential", "auth"}
)

// WriteError writes a sanitized error response to the client.
// It extracts the trace ID from the context, logs the full error internally,
// and returns a sanitized message to the client.
func WriteError(w http.ResponseWriter, r *http.Request, status int, err error, logger *logging.Logger) {
	traceID := logging.TraceIDFromContext(r.Context())
	if traceID == "" {
		traceID = generateTraceID()
	}

	// Log the full error internally with all details
	if logger != nil {
		logger.Error("request failed",
			"trace_id", traceID,
			"method", r.Method,
			"path", r.URL.Path,
			"status", status,
			"error", err.Error(),
			"client_ip", getClientIP(r),
		)
	}

	// Build sanitized response
	resp := ErrorResponse{
		Error:   sanitizeError(err.Error()),
		Code:    errorCodeFromStatus(status),
		TraceID: traceID,
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(resp)
}

// WriteErrorMessage writes a sanitized error response with a custom message.
func WriteErrorMessage(w http.ResponseWriter, r *http.Request, status int, message string, logger *logging.Logger) {
	traceID := logging.TraceIDFromContext(r.Context())
	if traceID == "" {
		traceID = generateTraceID()
	}

	if logger != nil {
		logger.Error("request failed",
			"trace_id", traceID,
			"method", r.Method,
			"path", r.URL.Path,
			"status", status,
			"error", message,
		)
	}

	resp := ErrorResponse{
		Error:   sanitizeError(message),
		Code:    errorCodeFromStatus(status),
		TraceID: traceID,
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(resp)
}

// sanitizeError removes potentially sensitive information from error messages.
// It prevents information leakage to clients while preserving useful context.
func sanitizeError(msg string) string {
	if msg == "" {
		return "An error occurred"
	}

	// Remove file paths
	msg = pathPattern.ReplaceAllString(msg, "[path]")

	// Remove sensitive keywords and their values
	lowerMsg := strings.ToLower(msg)
	for _, keyword := range sensitiveKeywords {
		if strings.Contains(lowerMsg, keyword) {
			return "An error occurred"
		}
	}

	// Remove internal error details
	msg = strings.ReplaceAll(msg, "internal error", "an error occurred")
	msg = strings.ReplaceAll(msg, "Internal Error", "an error occurred")

	// Truncate if too long
	if len(msg) > 200 {
		msg = msg[:200] + "..."
	}

	return msg
}

// errorCodeFromStatus returns the appropriate error code for an HTTP status.
func errorCodeFromStatus(status int) string {
	if code, ok := statusToCode[status]; ok {
		return code
	}
	return ErrCodeInternal
}

// getClientIP extracts the client IP from the request.
func getClientIP(r *http.Request) string {
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
		if idx := strings.Index(xff, ","); idx != -1 {
			return strings.TrimSpace(xff[:idx])
		}
		return strings.TrimSpace(xff)
	}
	if xri := r.Header.Get("X-Real-IP"); xri != "" {
		return strings.TrimSpace(xri)
	}
	return r.RemoteAddr
}

// generateTraceID generates a new trace ID when one isn't in context.
func generateTraceID() string {
	return fmt.Sprintf("%d", time.Now().UnixNano())
}
