package middleware

import (
	"errors"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/kernel/pkg/apierr"
	"github.com/nexuscrm/kernel/pkg/logging"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
)

// httpStatuser is implemented by pkg/errors types that carry an HTTP status code.
type httpStatuser interface {
	HTTPStatus() int
	Code() string
}

// InternalErrorProjector lets an L1 product/dialect supply the wire body for a
// generic internal (5xx) error on the routes it owns. It returns (0, nil) when it
// does not apply to the current request, in which case app-blind L0 emits its
// neutral default. App-blind L0 never knows the product's wire vocabulary; the
// projector decides per-request whether to apply.
type InternalErrorProjector func(c *gin.Context) (status int, body any)

type errorHandlerConfig struct {
	internalError InternalErrorProjector
}

// Option configures GlobalErrorHandler.
type Option func(*errorHandlerConfig)

// WithInternalErrorProjector injects an L1-supplied projector for the generic
// internal-error response. Without it (or when the projector returns nil for the
// request), L0 emits a neutral, product-agnostic 500 envelope.
func WithInternalErrorProjector(p InternalErrorProjector) Option {
	return func(cfg *errorHandlerConfig) { cfg.internalError = p }
}

// GlobalErrorHandler is a middleware that handles errors from the context.
func GlobalErrorHandler(logger *logging.Logger, opts ...Option) gin.HandlerFunc {
	var cfg errorHandlerConfig
	for _, o := range opts {
		o(&cfg)
	}

	// genericInternal renders the generic internal-error response: the injected
	// L1 projector if it applies to this request, else the app-blind neutral 500.
	genericInternal := func(c *gin.Context) (int, any) {
		if cfg.internalError != nil {
			if status, body := cfg.internalError(c); body != nil {
				if status == 0 {
					status = http.StatusInternalServerError
				}
				return status, body
			}
		}
		return http.StatusInternalServerError, apierr.New(
			http.StatusInternalServerError, "INTERNAL_ERROR",
			"An internal server error occurred. Contact your system administrator.")
	}

	return func(c *gin.Context) {
		c.Next() // Execute handlers

		// Check if there are any errors in the context
		if len(c.Errors) > 0 {
			// Get the last error
			err := c.Errors.Last().Err

			// Observational only (#3295, Slice 3): classify the error that is
			// about to be rendered into a bounded `cause` bucket and count it. This
			// is the single point where the final error is known. It reads the
			// error, increments a counter, and returns nothing — it does NOT alter
			// the status, body, or any render branch below.
			recordErrorCause(err)

			infrastructure := sharedErrors.ClassifyInfrastructure(err)
			if infrastructure == sharedErrors.InfrastructureConditionInternal {
				logger.WithField("path", c.Request.URL.Path).Error("Internal error", err)
				status, body := genericInternal(c)
				c.JSON(status, body)
				return
			}

			var apiErr *apierr.Error

			// Check if it's already an API Error
			if errors.As(err, &apiErr) {
				// Log internal cause if present
				if apiErr.Cause != nil {
					logger.WithField("code", apiErr.Code).Error("API Error with cause", apiErr.Cause)
				}
				if apiErr.Status >= http.StatusInternalServerError {
					logger.WithField("path", c.Request.URL.Path).Error("API internal error", err)
					status, body := genericInternal(c)
					c.JSON(status, body)
					return
				}
				c.JSON(apiErr.Status, apiErr)
				return
			}

			// Neutral engine-condition errors belong to the active API dialect. The
			// gin-free dialect binder projects them before they reach global
			// middleware; a leaked DomainWireError here means projection was missing
			// or incomplete, so fail closed with the generic internal envelope.
			var wireErr *sharedErrors.DomainWireError
			if errors.As(err, &wireErr) {
				logger.
					WithField("wire_error_kind", string(wireErr.Kind)).
					WithField("path", c.Request.URL.Path).
					Error("Domain wire error was not projected", err)
				status, body := genericInternal(c)
				c.JSON(status, body)
				return
			}

			// Check if it's a pkg/errors domain error (NotFoundError, PermissionError, etc.)
			var statuser httpStatuser
			if errors.As(err, &statuser) {
				if statuser.HTTPStatus() >= http.StatusInternalServerError {
					logger.WithField("path", c.Request.URL.Path).Error("Internal status error", err)
					status, body := genericInternal(c)
					c.JSON(status, body)
					return
				}
				mapped := apierr.New(statuser.HTTPStatus(), statuser.Code(), err.Error())
				c.JSON(mapped.Status, mapped)
				return
			}

			// A neutral retriable marker is a fallback for errors that do not
			// already own a more specific representation at this HTTP boundary.
			// In particular, legacy lock-contention errors deliberately remain
			// HTTP 409 here even though Runtime RPC maps the same condition to an
			// unavailable attempt. Internal wrappers and explicit API/domain
			// errors were handled above so a retriable cause cannot override the
			// owning boundary's classification. This post-handler boundary cannot
			// prove that business execution never began, so it must not emit
			// Retry-After: public Retry-After is reserved for pre-execution gates.
			if infrastructure == sharedErrors.InfrastructureConditionRetriable {
				logger.WithField("path", c.Request.URL.Path).Error("Retriable service error", err)
				_, body := genericInternal(c)
				c.JSON(http.StatusServiceUnavailable, body)
				return
			}

			// Prepare generic internal error
			logger.WithField("path", c.Request.URL.Path).Error("Unhandled error", err)
			status, body := genericInternal(c)
			c.JSON(status, body)
		}
	}
}
