package logger

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/google/uuid"
)

// AuditLogEntry holds common audit logging fields.
type AuditLogEntry struct {
	RequestID        string
	Stage            string
	Operation        string // e.g., "SendPasswordReset", "Verify"
	Method           string
	Path             string
	Status           int
	LatencyMs        time.Duration
	IP               string
	UserAgent        string
	Origin           string
	Referer          string
	Query            map[string]string
	Headers          map[string]string // 핵심 헤더(민감 키는 마스킹됨)
	LoginIDs         map[string]string // loginId and loginId_normalized
	Token            string            // For reset tokens, magic link tokens
	ProviderError    string
	ProviderStatus   int    // Provider HTTP status
	ProviderBody     string // Provider response body (full raw)
	RefreshToken     string
	SessionJwt       string
	AccessJwt        string
	UserLoginId      string
	UserID           string
	Email            string
	Phone            string
	SetCookieName    string
	SetCookieValue   string
	SetCookieAttrs   map[string]string
	RedirectTo       string
	HasCookieDSRF    bool
	ParsedCookieDSRF string
	// ... potentially more fields specific to different stages
}

// NewAuditLogEntry creates a new AuditLogEntry with a generated RequestID and initial common fields.
func NewAuditLogEntry(c *fiber.Ctx, stage string) *AuditLogEntry {
	reqID := uuid.New().String()

	// Extract query parameters
	queryParams := make(map[string]string)
	c.Context().QueryArgs().VisitAll(func(key, value []byte) {
		k := string(key)
		queryParams[k] = maskSensitiveByKey(k, string(value))
	})

	// Extract relevant headers
	headers := make(map[string]string)
	headers["Host"] = c.Get("Host")
	headers["User-Agent"] = c.Get("User-Agent")
	headers["Origin"] = c.Get("Origin")
	headers["Referer"] = c.Get("Referer")

	return &AuditLogEntry{
		RequestID:      reqID,
		Stage:          stage,
		Method:         c.Method(),
		Path:           c.Path(),
		IP:             c.IP(),
		UserAgent:      c.Get("User-Agent"),
		Origin:         c.Get("Origin"),
		Referer:        c.Get("Referer"),
		Query:          queryParams,
		Headers:        headers,
		LoginIDs:       make(map[string]string),
		SetCookieAttrs: make(map[string]string),
	}
}

// Log emits an audit log entry using slog.
// It includes common fields and allows for additional custom fields.
func (ale *AuditLogEntry) Log(level slog.Level, msg string, args ...any) {
	attrs := []slog.Attr{
		slog.String("req_id", ale.RequestID),
		slog.String("stage", ale.Stage),
	}

	if ale.Operation != "" {
		attrs = append(attrs, slog.String("op", ale.Operation))
	}
	if ale.Method != "" {
		attrs = append(attrs, slog.String("method", ale.Method))
	}
	if ale.Path != "" {
		attrs = append(attrs, slog.String("path", ale.Path))
	}
	if ale.Status != 0 {
		attrs = append(attrs, slog.Int("status", ale.Status))
	}
	if ale.LatencyMs != 0 {
		attrs = append(attrs, slog.Duration("latency_ms", ale.LatencyMs))
	}
	if ale.IP != "" {
		attrs = append(attrs, slog.String("ip", ale.IP))
	}
	if ale.UserAgent != "" {
		attrs = append(attrs, slog.String("user_agent", ale.UserAgent))
	}
	if ale.Origin != "" {
		attrs = append(attrs, slog.String("origin", ale.Origin))
	}
	if ale.Referer != "" {
		attrs = append(attrs, slog.String("referer", ale.Referer))
	}
	if len(ale.Query) > 0 {
		queryGroupArgs := make([]any, 0, len(ale.Query))
		for k, v := range ale.Query {
			queryGroupArgs = append(queryGroupArgs, slog.String(k, maskSensitiveByKey(k, v)))
		}
		attrs = append(attrs, slog.Group("query", queryGroupArgs...))
	}
	if len(ale.Headers) > 0 {
		headersGroupArgs := make([]any, 0, len(ale.Headers))
		for k, v := range ale.Headers {
			headersGroupArgs = append(headersGroupArgs, slog.String(k, maskSensitiveByKey(k, v)))
		}
		attrs = append(attrs, slog.Group("headers", headersGroupArgs...))
	}
	if len(ale.LoginIDs) > 0 {
		loginIDGroupArgs := make([]any, 0, len(ale.LoginIDs))
		for k, v := range ale.LoginIDs {
			loginIDGroupArgs = append(loginIDGroupArgs, slog.String(k, v))
		}
		attrs = append(attrs, slog.Group("login_ids", loginIDGroupArgs...))
	}
	if ale.Token != "" {
		attrs = append(attrs, slog.Bool("has_token", true))
	}
	if ale.ProviderError != "" {
		attrs = append(attrs, slog.String("provider_error", ale.ProviderError))
	}
	if ale.ProviderStatus != 0 {
		attrs = append(attrs, slog.Int("provider_http_status", ale.ProviderStatus))
	}
	if ale.ProviderBody != "" {
		attrs = append(attrs, slog.String("provider_response_body", ale.ProviderBody))
	}
	if ale.RefreshToken != "" {
		attrs = append(attrs, slog.Bool("has_refresh_token", true))
	}
	if ale.SessionJwt != "" {
		attrs = append(attrs, slog.Bool("has_session_jwt", true))
	}
	if ale.AccessJwt != "" {
		attrs = append(attrs, slog.Bool("has_access_jwt", true))
	}
	if ale.UserLoginId != "" {
		attrs = append(attrs, slog.String("user_login_id", ale.UserLoginId))
	}
	if ale.UserID != "" {
		attrs = append(attrs, slog.String("user_id", ale.UserID))
	}
	if ale.Email != "" {
		attrs = append(attrs, slog.String("email", ale.Email))
	}
	if ale.Phone != "" {
		attrs = append(attrs, slog.String("phone", ale.Phone))
	}
	if ale.SetCookieName != "" {
		attrs = append(attrs, slog.String("set_cookie_name", ale.SetCookieName))
		if ale.SetCookieValue != "" {
			attrs = append(attrs, slog.Bool("has_set_cookie_value", true))
		}
		if len(ale.SetCookieAttrs) > 0 {
			cookieAttrsGroupArgs := make([]any, 0, len(ale.SetCookieAttrs))
			for k, v := range ale.SetCookieAttrs {
				cookieAttrsGroupArgs = append(cookieAttrsGroupArgs, slog.String(k, v))
			}
			attrs = append(attrs, slog.Group("set_cookie_attrs", cookieAttrsGroupArgs...))
		}
	}
	if ale.RedirectTo != "" {
		attrs = append(attrs, slog.String("redirect_to", ale.RedirectTo))
	}
	if ale.HasCookieDSRF {
		attrs = append(attrs, slog.Bool("has_cookie_DSRF", ale.HasCookieDSRF))
	}
	if ale.ParsedCookieDSRF != "" {
		attrs = append(attrs, slog.Bool("has_parsed_cookie_DSRF", true))
	}

	// Convert variadic args to slog.Attr before appending
	for i := 0; i < len(args); i += 2 {
		if i+1 < len(args) {
			attrs = append(attrs, slog.Any(fmt.Sprintf("%v", args[i]), args[i+1]))
		} else {
			// Handle odd number of arguments - log the last one with a generic key
			attrs = append(attrs, slog.Any(fmt.Sprintf("extra_arg_%d", i), args[i]))
		}
	}

	slog.Default().LogAttrs(context.Background(), level, msg, attrs...)
}

var sensitiveAuditKeys = map[string]struct{}{
	"password":         {},
	"currentpassword":  {},
	"newpassword":      {},
	"oldpassword":      {},
	"token":            {},
	"accesstoken":      {},
	"refreshtoken":     {},
	"authorization":    {},
	"cookie":           {},
	"setcookie":        {},
	"verificationcode": {},
	"code":             {},
	"loginchallenge":   {},
	"loginverifier":    {},
	"sessionjwt":       {},
	"accessjwt":        {},
	"refreshjwt":       {},
}

func maskSensitiveByKey(key, value string) string {
	if value == "" {
		return value
	}
	k := strings.ToLower(key)
	k = strings.ReplaceAll(k, "-", "")
	k = strings.ReplaceAll(k, "_", "")
	if _, ok := sensitiveAuditKeys[k]; ok {
		return "*****"
	}
	return value
}
