package middleware

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"io"
	"kra/internal/biz/system"
	"log/slog"
	"mime"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"kra/internal/conf"
	"kra/internal/routecatalog"

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

const defaultRequestBodyLimit int64 = 8 << 20

// AccessLog is the single global request/response capture point, matching
// the reference middleware ordering and making every HTTP request observable.
func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.HandlerFunc {
	return func(c *gin.Context) {
		started := time.Now()
		var requestBody []byte
		multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
		mediaUpload := multipart && isMediaUploadRoute(c.FullPath())
		var config *conf.AdminBackend
		if runtime != nil {
			config = runtime.Admin()
		}
		bodyLimit := defaultRequestBodyLimit
		if mediaUpload {
			bodyLimit = system.DefaultMaxMediaFileSize + (1 << 20)
			if config != nil && config.Media != nil && config.Media.MaxFileSize > 0 {
				bodyLimit = config.Media.MaxFileSize + (1 << 20)
			}
		}
		bytesIn := c.Request.ContentLength
		maxBytes := 1 << 20
		writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
		c.Writer = writer
		c.Header("X-Kra-Version", version)
		requestReadFailed := c.Request.ContentLength > bodyLimit
		if requestReadFailed {
			c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": CodeError, "msg": "请求体超过大小上限"})
		} else if c.Request.Body != nil && !mediaUpload {
			limited := http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
			var err error
			requestBody, err = io.ReadAll(limited)
			if err != nil {
				requestReadFailed = true
				var tooLarge *http.MaxBytesError
				if errors.As(err, &tooLarge) {
					c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": CodeError, "msg": "请求体超过大小上限"})
				} else {
					c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": CodeError, "msg": "请求体读取失败"})
				}
			} else {
				c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
				bytesIn = int64(len(requestBody))
			}
		} else if c.Request.Body != nil {
			c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
		}
		if bytesIn < 0 {
			bytesIn = 0
		}
		logLimit := 1024
		if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
			logLimit = int(config.Zap.AccessLogMaxBytes)
		}
		bodyPolicy := routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path)
		paymentCallback := bodyPolicy == routecatalog.BodyPolicyPaymentCallback
		paymentConfigWrite := bodyPolicy == routecatalog.BodyPolicyPaymentConfig
		requestText := ""
		if paymentCallback {
			requestText = paymentCallbackSummary(requestBody, c.GetHeader("Content-Type"))
		} else if paymentConfigWrite {
			requestText = paymentConfigSummary(requestBody)
		} else if multipart {
			requestText = "[文件]"
		} else {
			requestText = redactJSON(requestBody, c.GetHeader("Content-Type"), logLimit)
		}
		c.Set(ctxReqBodyKey, requestText)
		if paymentCallback {
			// Callback acknowledgements and provider payloads must not flow into
			// the generic response/error audit pipeline.
			c.Set(ctxRespBufferKey, &bytes.Buffer{})
		} else {
			c.Set(ctxRespBufferKey, &writer.body)
		}
		if !requestReadFailed {
			c.Next()
		}
		if logger == nil {
			return
		}
		responseText := redactJSON(writer.body.Bytes(), c.Writer.Header().Get("Content-Type"), logLimit)
		if paymentCallback {
			responseText = "[支付回调响应已省略]"
		}
		userID, authorityID := uint(0), uint(0)
		if claims := Claims(c); claims != nil {
			userID, authorityID = claims.ID, claims.AuthorityID
		}
		route := c.FullPath()
		if route == "" {
			route = "unmatched"
		}
		bytesOut := int64(c.Writer.Size())
		if bytesOut < 0 {
			bytesOut = 0
		}
		privateErrors := strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n")
		var attributes []any
		if paymentCallback {
			attributes = []any{
				"mod", "payment-callback", "payment_provider", paymentCallbackProvider(c.Request.URL.Path),
				"http_status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(),
				"request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"),
				"bytes_in", bytesIn, "bytes_out", bytesOut,
				"error", c.Writer.Status() >= http.StatusInternalServerError || privateErrors != "",
				"payment_callback", true, "payment_callback_summary", requestText,
			}
		} else {
			attributes = []any{
				"mod", "http", "ip", c.ClientIP(), "method", c.Request.Method, "http_path", c.Request.URL.Path, "http_route", route,
				"http_status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(),
				"request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"),
				"bytes_in", bytesIn, "bytes_out", bytesOut, "user_id", userID, "authority_id", authorityID,
				"error", c.Writer.Status() >= http.StatusInternalServerError || privateErrors != "", "ua", c.Request.UserAgent(),
				"req_query", redactQuery(c.Request.URL.RawQuery),
			}
		}
		if !paymentCallback && config != nil && config.Zap != nil && config.Zap.AccessReqHeaders {
			attributes = append(attributes, "req_headers", redactHeaders(c.Request.Header))
		}
		if !paymentCallback && config != nil && config.Zap != nil && config.Zap.AccessReqBody {
			attributes = append(attributes, "req_body", requestText)
		}
		if !paymentCallback && config != nil && config.Zap != nil && config.Zap.AccessRespData {
			attributes = append(attributes, "resp_data", responseText)
		}
		if !paymentCallback && privateErrors != "" {
			attributes = append(attributes, "error_msg", privateErrors)
		}
		logger.InfoContext(c.Request.Context(), "请求完成", attributes...)
	}
}

func isMediaUploadRoute(route string) bool {
	return strings.HasSuffix(route, "/fileUploadAndDownload/upload") ||
		strings.HasSuffix(route, "/mediaUpload/chunk")
}

func paymentCallbackProvider(path string) string {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	for index := 0; index+2 < len(parts); index++ {
		if parts[index] == "payment" && parts[index+1] == "callback" {
			return parts[index+2]
		}
	}
	return "unknown"
}

// isPaymentIntegrationConfigWrite is kept as a compatibility seam for
// middleware tests; policy ownership lives in routecatalog.
func isPaymentIntegrationConfigWrite(method, path string) bool {
	return routecatalog.BodyPolicyFor(method, path) == routecatalog.BodyPolicyPaymentConfig
}

func paymentCallbackSummary(body []byte, contentType string) string {
	mediaType, _, err := mime.ParseMediaType(contentType)
	if err != nil || mediaType == "" {
		mediaType = strings.TrimSpace(strings.SplitN(contentType, ";", 2)[0])
	}
	if mediaType == "" {
		mediaType = "unknown"
	}
	digest := sha256.Sum256(body)
	return "[支付回调正文已省略 body_bytes=" + strconv.Itoa(len(body)) + " body_sha256=" + hex.EncodeToString(digest[:]) + " content_type=" + mediaType + "]"
}

func paymentConfigSummary(body []byte) string {
	digest := sha256.Sum256(body)
	return "[支付配置正文已省略 body_bytes=" + strconv.Itoa(len(body)) + " body_sha256=" + hex.EncodeToString(digest[:]) + "]"
}

func redactHeaders(headers map[string][]string) map[string]string {
	out := make(map[string]string, len(headers))
	for key, values := range headers {
		lower := strings.ToLower(key)
		if lower == "authorization" || lower == "cookie" || lower == "set-cookie" || lower == "x-token" {
			out[key] = "***"
		} else {
			out[key] = strings.Join(values, ",")
		}
	}
	return out
}

func redactQuery(raw string) string {
	if strings.TrimSpace(raw) == "" {
		return ""
	}
	values, _ := url.ParseQuery(raw)
	for key := range values {
		if sensitiveQueryKey(key) {
			values[key] = []string{"***"}
		}
	}
	return values.Encode()
}

func sensitiveQueryKey(key string) bool {
	normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
	switch normalized {
	case "token", "accesstoken", "refreshtoken", "authorization", "apikey", "secret":
		return true
	default:
		return strings.HasSuffix(normalized, "token")
	}
}

func stringValueFromContext(c *gin.Context, key string) string {
	value, _ := c.Get(key)
	return stringValue(value)
}
