package middleware

import (
	"net/http"
	"runtime/debug"
	"time"

	"veza-backend-api/internal/errors"
	errorMetricsPkg "veza-backend-api/internal/metrics"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// ErrorHandler middleware pour gérer toutes les erreurs de manière standardisée
func ErrorHandler(logger *zap.Logger, errorMetrics *errorMetricsPkg.ErrorMetrics, includeStackTrace bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()

		// Traiter les erreurs stockées dans le contexte
		if len(c.Errors) > 0 {
			err := c.Errors.Last().Err

			// Vérifier si c'est une AppError personnalisée
			if appErr, ok := err.(*errors.AppError); ok {
				// Enrichir l'erreur avec le contexte de la requête
				enrichErrorWithContext(c, appErr)

				httpStatus := mapErrorCodeToHTTPStatus(appErr.Code)

				// Enregistrer l'erreur dans les métriques (T0020)
				if errorMetrics != nil {
					errorMetrics.RecordError(appErr.Code, httpStatus)
				}

				// Enregistrer l'erreur dans Prometheus (T0021)
				errorMetricsPkg.RecordErrorPrometheus(appErr.Code, httpStatus)

				// Logger structuré avec contexte complet (T0028)
				logFields := []zap.Field{
					zap.Int("code", int(appErr.Code)),
					zap.String("message", appErr.Message),
					zap.Int("http_status", httpStatus),
				}

				// Ajouter les champs de contexte au logger si disponibles
				if appErr.Context != nil {
					if requestID, ok := appErr.Context["request_id"].(string); ok {
						logFields = append(logFields, zap.String("request_id", requestID))
					}
					if userID, ok := appErr.Context["user_id"]; ok {
						logFields = append(logFields, zap.Any("user_id", userID))
					}
				}

				// Ajouter trace_id et span_id si disponibles (T0025)
				if traceID := GetTraceID(c); traceID != "" {
					logFields = append(logFields, zap.String("trace_id", traceID))
				}
				if spanID := GetSpanID(c); spanID != "" {
					logFields = append(logFields, zap.String("span_id", spanID))
				}

				// Ajouter l'erreur causale si présente
				if appErr.Err != nil {
					logFields = append(logFields, zap.Error(appErr.Err))
				}

				// Ajouter les détails de validation si présents
				if len(appErr.Details) > 0 {
					logFields = append(logFields, zap.Any("details", appErr.Details))
				}

				// Logger au niveau ERROR avec format JSON structuré
				logger.Error("Application error", logFields...)

				// INT-006: Standardize error response format using APIResponse
				errorData := struct {
					Code      int                    `json:"code"`
					Message   string                 `json:"message"`
					Details   []errors.ErrorDetail   `json:"details,omitempty"`
					RequestID string                 `json:"request_id,omitempty"`
					Timestamp string                 `json:"timestamp"`
					Context   map[string]interface{} `json:"context,omitempty"`
				}{
					Code:      int(appErr.Code),
					Message:   appErr.Message,
					Details:   appErr.Details,
					RequestID: c.GetString("request_id"),
					Timestamp: time.Now().UTC().Format(time.RFC3339),
					Context:   appErr.Context,
				}

				type APIResponse struct {
					Success bool        `json:"success"`
					Data    interface{} `json:"data,omitempty"`
					Error   interface{} `json:"error,omitempty"`
				}

				c.JSON(httpStatus, APIResponse{
					Success: false,
					Data:    nil,
					Error:   errorData,
				})
				return
			}

			// Vérifier si c'est une erreur GORM
			if err == gorm.ErrRecordNotFound {
				// Enregistrer l'erreur dans les métriques (T0020)
				if errorMetrics != nil {
					errorMetrics.RecordError(errors.ErrCodeNotFound, http.StatusNotFound)
				}

				// Enregistrer l'erreur dans Prometheus (T0021)
				errorMetricsPkg.RecordErrorPrometheus(errors.ErrCodeNotFound, http.StatusNotFound)

				// Logger structuré avec contexte
				logFields := []zap.Field{
					zap.Int("code", int(errors.ErrCodeNotFound)),
					zap.String("message", "Resource not found"),
					zap.Int("http_status", http.StatusNotFound),
					zap.Error(err),
				}

				// Ajouter request_id si disponible
				if requestID, exists := c.Get("request_id"); exists {
					if requestIDStr, ok := requestID.(string); ok {
						logFields = append(logFields, zap.String("request_id", requestIDStr))
					}
				}

				// Ajouter trace_id et span_id si disponibles (T0025)
				if traceID := GetTraceID(c); traceID != "" {
					logFields = append(logFields, zap.String("trace_id", traceID))
				}
				if spanID := GetSpanID(c); spanID != "" {
					logFields = append(logFields, zap.String("span_id", spanID))
				}

				logger.Warn("Record not found", logFields...)

				// INT-006: Standardize error response format using APIResponse
				errorData := struct {
					Code      int    `json:"code"`
					Message   string `json:"message"`
					RequestID string `json:"request_id,omitempty"`
					Timestamp string `json:"timestamp"`
				}{
					Code:      int(errors.ErrCodeNotFound),
					Message:   "Resource not found",
					RequestID: c.GetString("request_id"),
					Timestamp: time.Now().UTC().Format(time.RFC3339),
				}

				type APIResponse struct {
					Success bool        `json:"success"`
					Data    interface{} `json:"data,omitempty"`
					Error   interface{} `json:"error,omitempty"`
				}

				c.JSON(http.StatusNotFound, APIResponse{
					Success: false,
					Data:    nil,
					Error:   errorData,
				})
				return
			}

			// Erreur générique - logging structuré avec stack trace (T0028)
			// Enregistrer l'erreur dans les métriques (T0020)
			if errorMetrics != nil {
				errorMetrics.RecordError(errors.ErrCodeInternal, http.StatusInternalServerError)
			}

			// Enregistrer l'erreur dans Prometheus (T0021)
			errorMetricsPkg.RecordErrorPrometheus(errors.ErrCodeInternal, http.StatusInternalServerError)

			// Logger structuré avec contexte complet et stack trace
			logFields := []zap.Field{
				zap.Int("code", int(errors.ErrCodeInternal)),
				zap.String("message", "Internal server error"),
				zap.Int("http_status", http.StatusInternalServerError),
				zap.Error(err),
			}

			// Ajouter stack trace uniquement si configuré (MOD-P1-006)
			if includeStackTrace {
				logFields = append(logFields, zap.ByteString("stack_trace", debug.Stack()))
			}

			// Ajouter request_id si disponible
			if requestID, exists := c.Get("request_id"); exists {
				if requestIDStr, ok := requestID.(string); ok {
					logFields = append(logFields, zap.String("request_id", requestIDStr))
				}
			}

			// Ajouter user_id si disponible
			if userID, exists := c.Get("user_id"); exists {
				logFields = append(logFields, zap.Any("user_id", userID))
			}

			// Ajouter trace_id et span_id si disponibles (T0025)
			if traceID := GetTraceID(c); traceID != "" {
				logFields = append(logFields, zap.String("trace_id", traceID))
			}
			if spanID := GetSpanID(c); spanID != "" {
				logFields = append(logFields, zap.String("span_id", spanID))
			}

			// Logger au niveau ERROR avec format JSON structuré
			logger.Error("Internal server error", logFields...)

			// INT-006: Standardize error response format using APIResponse
			errorData := struct {
				Code      int    `json:"code"`
				Message   string `json:"message"`
				RequestID string `json:"request_id,omitempty"`
				Timestamp string `json:"timestamp"`
			}{
				Code:      int(errors.ErrCodeInternal),
				Message:   "Internal server error",
				RequestID: c.GetString("request_id"),
				Timestamp: time.Now().UTC().Format(time.RFC3339),
			}

			type APIResponse struct {
				Success bool        `json:"success"`
				Data    interface{} `json:"data,omitempty"`
				Error   interface{} `json:"error,omitempty"`
			}

			c.JSON(http.StatusInternalServerError, APIResponse{
				Success: false,
				Data:    nil,
				Error:   errorData,
			})
		}
	}
}

// enrichErrorWithContext enrichit une AppError avec le contexte de la requête (request_id, user_id)
func enrichErrorWithContext(c *gin.Context, appErr *errors.AppError) {
	if appErr.Context == nil {
		appErr.Context = make(map[string]interface{})
	}

	// Ajouter le request_id depuis le contexte Gin
	if requestID, exists := c.Get("request_id"); exists {
		if requestIDStr, ok := requestID.(string); ok {
			appErr.Context["request_id"] = requestIDStr
		}
	}

	// Ajouter le user_id depuis le contexte Gin si disponible
	if userID, exists := c.Get("user_id"); exists {
		appErr.Context["user_id"] = userID
	}
}

// mapErrorCodeToHTTPStatus convertit un code d'erreur en status HTTP
func mapErrorCodeToHTTPStatus(code errors.ErrorCode) int {
	switch {
	case code >= 1000 && code < 2000:
		if code == errors.ErrCodeForbidden {
			return http.StatusForbidden
		}
		return http.StatusUnauthorized
	case code >= 2000 && code < 3000:
		return http.StatusBadRequest
	case code >= 3000 && code < 4000:
		if code == errors.ErrCodeNotFound {
			return http.StatusNotFound
		}
		if code == errors.ErrCodeConflict || code == errors.ErrCodeAlreadyExists {
			return http.StatusConflict
		}
		return http.StatusBadRequest
	case code >= 5000 && code < 6000:
		return http.StatusTooManyRequests
	default:
		return http.StatusInternalServerError
	}
}
