package middleware

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"kra/internal/config"
	"kra/internal/routecatalog"
	"kra/internal/service/dto"
	systemservice "kra/internal/service/system"

	"github.com/gin-gonic/gin"
)

const ctxOperationAuditPersistFailedKey = "operation_audit_persist_failed"

func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder) gin.HandlerFunc {
	return func(c *gin.Context) {
		if runtime == nil || recorder == nil {
			c.Next()
			return
		}
		routePath := c.FullPath()
		if routePath == "" {
			routePath = c.Request.URL.Path
		}
		path := c.Request.URL.Path
		if !routecatalog.ShouldAudit(c.Request.Method, routePath) {
			c.Next()
			return
		}
		var requestBody []byte
		maxBytes := 1024
		if snapshot := runtime.Snapshot(); snapshot != nil && snapshot.Admin != nil && snapshot.Admin.Zap != nil && snapshot.Admin.Zap.AccessLogMaxBytes > 0 {
			maxBytes = snapshot.Admin.Zap.AccessLogMaxBytes
		}
		if c.Request.Method == http.MethodGet {
			requestBody = operationQueryBody(c.Request.URL.RawQuery)
		} else if _, ok := c.Get(ctxReqBodyKey); ok {
		} else if c.Request.Body != nil {
			// Fallback for tests or custom middleware chains that omit AccessLog.
			if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
				requestBody = []byte("[文件]")
			} else {
				limited := http.MaxBytesReader(c.Writer, c.Request.Body, defaultRequestBodyLimit)
				var err error
				requestBody, err = io.ReadAll(limited)
				if err != nil {
					var tooLarge *http.MaxBytesError
					if errors.As(err, &tooLarge) {
						c.AbortWithStatus(http.StatusRequestEntityTooLarge)
					} else {
						c.AbortWithStatus(http.StatusBadRequest)
					}
					return
				}
				c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
			}
		}
		started := time.Now()
		c.Next()
		userID := uint(0)
		if claims := Claims(c); claims != nil {
			userID = claims.ID
		}
		requestID, _ := c.Get("request_id")
		status := c.Writer.Status()
		responseBody := ""
		if value, ok := c.Get(ctxRespTextKey); ok {
			responseBody = stringValue(value)
		} else if value, ok := c.Get(ctxRespBufferKey); ok {
			if body, bok := value.(*bytes.Buffer); bok {
				responseBody = redactJSON(body.Bytes(), c.Writer.Header().Get("Content-Type"), maxBytes)
			}
		}
		if truncated, _ := c.Get(ctxRespTruncatedKey); truncated == true {
			responseBody = "[超出记录长度]"
		}
		errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String()
		operationBody := ""
		capturedBody := false
		if c.Request.Method != "GET" {
			if value, ok := c.Get(ctxReqBodyKey); ok {
				// AccessLog has already captured, redacted, summarized, and bounded the
				// request body. Treat that value as final so audit does not process it a
				// second time.
				operationBody = stringValue(value)
				capturedBody = true
			}
		}
		if !capturedBody {
			operationBody = operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes)
		}
		if !capturedBody && routecatalog.BodyPolicyFor(c.Request.Method, path) == routecatalog.BodyPolicyPaymentConfig {
			operationBody = paymentConfigSummary(requestBody)
		}
		if err := recorder.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: operationBody, Response: responseBody, UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")}); err != nil {
			// Preserve the business response, but expose audit persistence failures
			// to the global access/error logging pipeline.
			c.Set(ctxOperationAuditPersistFailedKey, true)
			_ = c.Error(fmt.Errorf("operation audit persist: %w", err))
		}
	}
}

func operationQueryBody(raw string) []byte {
	parsed, _ := url.ParseQuery(raw)
	values := make(map[string]string, len(parsed))
	for key, items := range parsed {
		if len(items) > 0 {
			value := items[len(items)-1]
			if isSensitivePayloadKey(key) {
				value = redactedValue
			}
			values[key] = value
		}
	}
	body, _ := json.Marshal(&values)
	return body
}

func operationRequestBody(raw []byte, contentType string, limit int) string {
	if strings.Contains(contentType, "multipart/form-data") {
		return "[文件]"
	}
	text := string(raw)
	if strings.Contains(strings.ToLower(contentType), "json") && text != "" {
		var value any
		if json.Unmarshal(raw, &value) == nil {
			maskOperationBody(value)
			if encoded, err := json.Marshal(value); err == nil {
				text = string(encoded)
			}
		}
	}
	if len(text) > limit {
		return "[超出记录长度]"
	}
	return text
}

func maskOperationBody(value any) {
	switch current := value.(type) {
	case map[string]any:
		for key, item := range current {
			if isSensitivePayloadKey(key) {
				current[key] = redactedValue
				continue
			}
			maskOperationBody(item)
		}
	case []any:
		for _, item := range current {
			maskOperationBody(item)
		}
	}
}
