package service

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"strings"
	"time"

	"wucher/internal/domain/audit"
)

type AuditLogInput struct {
	RequestID   string
	ActorUserID []byte
	Layer       string
	Module      string
	Action      string
	Method      string
	Path        string
	StatusCode  int
	Success     bool
	IP          string
	UserAgent   string
	Message     string
	Metadata    map[string]any
}

type AuditLogService struct {
	repo        audit.Repository
	queue       chan *audit.AuditLog
	ipProtector SecretProtector
}

type AuditLogServiceDependencies struct {
	QueueSize       int
	Workers         int
	IPEncryptionKey string
}

type auditLogListRepository interface {
	List(ctx context.Context, filter string, methods []string, modules []string, actions []string, sort string, limit, offset int) ([]audit.AuditLog, int64, error)
}

func NewAuditLogService(repo audit.Repository, deps AuditLogServiceDependencies) *AuditLogService {
	queueSize := deps.QueueSize
	workers := deps.Workers
	if queueSize <= 0 {
		queueSize = 1024
	}
	if workers <= 0 {
		workers = 2
	}
	key := strings.TrimSpace(deps.IPEncryptionKey)
	svc := &AuditLogService{
		repo:        repo,
		queue:       make(chan *audit.AuditLog, queueSize),
		ipProtector: newIPSecretProtector(key),
	}
	for i := 0; i < workers; i++ {
		go svc.runWorker()
	}
	return svc
}

func (s *AuditLogService) Log(ctx context.Context, in AuditLogInput) {
	if s == nil || s.repo == nil {
		return
	}
	entry := &audit.AuditLog{
		RequestID:   truncate(strings.TrimSpace(in.RequestID), 64),
		ActorUserID: safeUserID(in.ActorUserID),
		Layer:       truncate(strings.TrimSpace(in.Layer), 32),
		Module:      truncate(strings.TrimSpace(in.Module), 64),
		Action:      truncate(strings.TrimSpace(in.Action), 128),
		Method:      truncate(strings.TrimSpace(in.Method), 12),
		Path:        truncate(strings.TrimSpace(in.Path), 255),
		StatusCode:  in.StatusCode,
		Success:     in.Success,
		IP:          sanitizeIP(in.IP, s.ipProtector),
		UserAgent:   truncate(strings.TrimSpace(in.UserAgent), 255),
		Message:     truncate(strings.TrimSpace(in.Message), 512),
		CreatedAt:   time.Now().UTC(),
	}
	if len(in.Metadata) > 0 {
		if b, err := json.Marshal(in.Metadata); err == nil {
			entry.Metadata = truncateBytes(b, 8192)
		}
	}

	select {
	case s.queue <- entry:
	default:
		log.Printf("audit queue_full layer=%s action=%s", entry.Layer, entry.Action)
	}
}

func (s *AuditLogService) List(ctx context.Context, filter string, methods []string, modules []string, actions []string, sort string, limit, offset int) ([]audit.AuditLog, int64, error) {
	if s == nil || s.repo == nil {
		return nil, 0, errors.New("audit service not configured")
	}
	lister, ok := s.repo.(auditLogListRepository)
	if !ok {
		return nil, 0, errors.New("audit listing not supported by repository")
	}
	if limit < 0 {
		limit = 0
	}
	if limit > 200 {
		limit = 200
	}
	if offset < 0 {
		offset = 0
	}
	methods, actions = enforceAuditMutationFilters(methods, actions)
	rows, total, err := lister.List(
		ctx,
		strings.TrimSpace(filter),
		normalizeAuditMethods(methods),
		normalizeAuditTokens(modules),
		normalizeAuditTokens(actions),
		normalizeAuditSort(sort),
		limit,
		offset,
	)
	if err != nil {
		return nil, 0, err
	}
	for i := range rows {
		rows[i].IP = decryptStoredIP(rows[i].IP, s.ipProtector)
	}
	return rows, total, nil
}

func enforceAuditMutationFilters(methods, actions []string) ([]string, []string) {
	allowedMethods := []string{"POST", "PUT", "PATCH", "DELETE"}
	allowedActions := []string{"create", "update", "delete"}

	methods = enforceAllowedMethods(methods, allowedMethods)
	actions = enforceAllowedTokens(actions, allowedActions)
	return methods, actions
}

func enforceAllowedMethods(values []string, allowed []string) []string {
	if len(values) == 0 {
		return append([]string(nil), allowed...)
	}
	allowedSet := make(map[string]struct{}, len(allowed))
	for _, value := range allowed {
		allowedSet[strings.ToUpper(strings.TrimSpace(value))] = struct{}{}
	}
	out := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		v := strings.ToUpper(strings.TrimSpace(value))
		if v == "UPDATE" {
			if _, ok := allowedSet["PUT"]; ok {
				if _, exists := seen["PUT"]; !exists {
					seen["PUT"] = struct{}{}
					out = append(out, "PUT")
				}
			}
			if _, ok := allowedSet["PATCH"]; ok {
				if _, exists := seen["PATCH"]; !exists {
					seen["PATCH"] = struct{}{}
					out = append(out, "PATCH")
				}
			}
			continue
		}
		if _, ok := allowedSet[v]; !ok {
			continue
		}
		if _, exists := seen[v]; exists {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	if len(out) == 0 {
		return append([]string(nil), allowed...)
	}
	return out
}

func enforceAllowedTokens(values []string, allowed []string) []string {
	if len(values) == 0 {
		return append([]string(nil), allowed...)
	}
	allowedSet := make(map[string]struct{}, len(allowed))
	for _, value := range allowed {
		allowedSet[strings.ToLower(strings.TrimSpace(value))] = struct{}{}
	}
	out := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		v := strings.ToLower(strings.TrimSpace(value))
		if _, ok := allowedSet[v]; !ok {
			continue
		}
		if _, exists := seen[v]; exists {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	if len(out) == 0 {
		return append([]string(nil), allowed...)
	}
	return out
}

func (s *AuditLogService) runWorker() {
	for entry := range s.queue {
		if entry == nil {
			continue
		}
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		err := s.repo.Create(ctx, entry)
		cancel()
		if err != nil {
			log.Printf("audit persist_failed layer=%s action=%s err=%v", entry.Layer, entry.Action, err)
		}
	}
}

func safeUserID(id []byte) []byte {
	if len(id) != 16 {
		return nil
	}
	dst := make([]byte, 16)
	copy(dst, id)
	return dst
}

func truncateBytes(v []byte, max int) []byte {
	if max <= 0 || len(v) <= max {
		return v
	}
	out := make([]byte, max)
	copy(out, v[:max])
	return out
}

func normalizeAuditSort(sort string) string {
	if normalized := normalizeSortByRules(strings.TrimSpace(sort),
		sortField("created_at"),
		sortField("action"),
		sortField("module"),
		sortField("layer"),
		sortField("status_code"),
		sortField("success"),
	); normalized != "" {
		return normalized
	}
	return "created_at DESC"
}

func normalizeAuditTokens(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		v := strings.ToLower(strings.TrimSpace(value))
		switch v {
		case "", "all", "*":
			return nil
		default:
			if _, ok := seen[v]; !ok {
				seen[v] = struct{}{}
				out = append(out, v)
			}
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func normalizeAuditMethods(methods []string) []string {
	if len(methods) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(methods))
	out := make([]string, 0, len(methods))
	for _, method := range methods {
		v := strings.ToUpper(strings.TrimSpace(method))
		switch v {
		case "", "ALL", "*":
			return nil
		case "GET", "POST", "PUT", "PATCH", "DELETE":
			if _, ok := seen[v]; !ok {
				seen[v] = struct{}{}
				out = append(out, v)
			}
		case "UPDATE":
			if _, ok := seen["PUT"]; !ok {
				seen["PUT"] = struct{}{}
				out = append(out, "PUT")
			}
			if _, ok := seen["PATCH"]; !ok {
				seen["PATCH"] = struct{}{}
				out = append(out, "PATCH")
			}
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}
