package audit

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"
	"sync/atomic"
	"time"

	platformid "aigateway.local/core/internal/platform/id"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
)

type Event struct {
	TenantID         *string
	RequestID        string
	APIKeyID         *string
	ProviderCode     string
	Model            string
	Protocol         string
	StatusCode       int
	PromptTokens     int64
	CompletionTokens int64
	CostMicrounits   int64
	PriceID          string
	Currency         string
	LatencyMS        int
	Labels           map[string]any
	RecordedAt       time.Time
}

type Stats struct {
	Accepted uint64
	Dropped  uint64
	Written  uint64
	Failures uint64
}

type Recorder struct {
	pool          *pgxpool.Pool
	logger        *slog.Logger
	queue         chan Event
	batchSize     int
	flushInterval time.Duration
	accepted      atomic.Uint64
	dropped       atomic.Uint64
	written       atomic.Uint64
	failures      atomic.Uint64
}

func NewRecorder(pool *pgxpool.Pool, logger *slog.Logger, queueSize, batchSize int, flushInterval time.Duration) *Recorder {
	if queueSize < 1 {
		queueSize = 4096
	}
	if batchSize < 1 {
		batchSize = 200
	}
	if flushInterval <= 0 {
		flushInterval = time.Second
	}
	return &Recorder{pool: pool, logger: logger, queue: make(chan Event, queueSize), batchSize: batchSize, flushInterval: flushInterval}
}

func (r *Recorder) Record(event Event) bool {
	if r == nil || r.pool == nil {
		return false
	}
	if event.RecordedAt.IsZero() {
		event.RecordedAt = time.Now().UTC()
	}
	select {
	case r.queue <- event:
		r.accepted.Add(1)
		return true
	default:
		r.dropped.Add(1)
		return false
	}
}

func (r *Recorder) Stats() Stats {
	if r == nil {
		return Stats{}
	}
	return Stats{Accepted: r.accepted.Load(), Dropped: r.dropped.Load(), Written: r.written.Load(), Failures: r.failures.Load()}
}

func (r *Recorder) Prometheus() string {
	stats := r.Stats()
	return fmt.Sprintf("gateway_audit_events_accepted_total %d\ngateway_audit_events_dropped_total %d\ngateway_audit_events_written_total %d\ngateway_audit_flush_failures_total %d\n", stats.Accepted, stats.Dropped, stats.Written, stats.Failures)
}

func (r *Recorder) Run(ctx context.Context) {
	ticker := time.NewTicker(r.flushInterval)
	defer ticker.Stop()
	pending := make([]Event, 0, r.batchSize)
	flushAllowed := true
	for {
		var events <-chan Event = r.queue
		if len(pending) >= cap(r.queue)+r.batchSize {
			events = nil
		}
		select {
		case event := <-events:
			pending = append(pending, event)
			if len(pending) >= r.batchSize && flushAllowed {
				if r.flush(ctx, pending) == nil {
					pending = pending[:0]
				} else {
					flushAllowed = false
				}
			}
		case <-ticker.C:
			flushAllowed = true
			if len(pending) > 0 {
				if r.flush(ctx, pending) == nil {
					pending = pending[:0]
				} else {
					flushAllowed = false
				}
			}
		case <-ctx.Done():
			for {
				select {
				case event := <-r.queue:
					pending = append(pending, event)
				default:
					flushCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
					if len(pending) > 0 {
						_ = r.flush(flushCtx, pending)
					}
					cancel()
					return
				}
			}
		}
	}
}

type dailyKey struct {
	date, apiKeyID, provider, model string
}

type dailyValue struct {
	requests, failed, prompt, completion, cost int64
}

type policyAlert struct {
	eventID, requestID string
	tenantID           *string
	payload            []byte
}

func (r *Recorder) flush(ctx context.Context, events []Event) error {
	if len(events) == 0 {
		return nil
	}
	tx, err := r.pool.Begin(ctx)
	if err != nil {
		return r.flushError(err)
	}
	defer func() { _ = tx.Rollback(ctx) }()
	rows := make([][]any, 0, len(events))
	daily := make(map[dailyKey]dailyValue)
	alerts := make([]policyAlert, 0)
	for _, event := range events {
		id, err := platformid.NewUUID()
		if err != nil {
			return r.flushError(err)
		}
		labels, _ := json.Marshal(event.Labels)
		if policies, matched := event.Labels["content_policies"]; matched {
			alertID, idErr := platformid.NewUUID()
			if idErr != nil {
				return r.flushError(idErr)
			}
			payload, _ := json.Marshal(map[string]any{
				"request_id": event.RequestID, "protocol": event.Protocol, "status_code": event.StatusCode,
				"provider_code": event.ProviderCode, "model": event.Model, "content_policies": policies,
				"content_redacted": event.Labels["content_redacted"], "recorded_at": event.RecordedAt,
			})
			alerts = append(alerts, policyAlert{eventID: alertID, requestID: event.RequestID, tenantID: event.TenantID, payload: payload})
		}
		rows = append(rows, []any{
			uuidValue(id), uuidPointer(event.TenantID), event.RequestID, nil, uuidPointer(event.APIKeyID),
			nullString(event.ProviderCode), nullString(event.Model), event.Protocol, event.StatusCode,
			nullToken(event.PromptTokens), nullToken(event.CompletionTokens), nullCost(event.CostMicrounits), event.LatencyMS,
			nil, nil, labels, event.RecordedAt,
		})
		if event.APIKeyID != nil && *event.APIKeyID != "" {
			key := dailyKey{date: event.RecordedAt.UTC().Format("2006-01-02"), apiKeyID: *event.APIKeyID, provider: event.ProviderCode, model: event.Model}
			value := daily[key]
			value.requests++
			if event.StatusCode >= 400 {
				value.failed++
			}
			value.prompt += max(event.PromptTokens, 0)
			value.completion += max(event.CompletionTokens, 0)
			value.cost += max(event.CostMicrounits, 0)
			daily[key] = value
		}
	}
	_, err = tx.CopyFrom(ctx, pgx.Identifier{"gateway", "audit_events"}, []string{
		"id", "tenant_id", "request_id", "actor_id", "api_key_id", "provider_code", "model", "protocol",
		"status_code", "prompt_tokens", "completion_tokens", "cost_microunits", "latency_ms",
		"request_preview", "response_preview", "labels", "recorded_at",
	}, pgx.CopyFromRows(rows))
	if err != nil {
		return r.flushError(err)
	}
	batch := &pgx.Batch{}
	for key, value := range daily {
		batch.Queue(`INSERT INTO gateway.usage_daily(usage_date,api_key_id,provider_code,model,requests,failed_requests,prompt_tokens,completion_tokens,cost_microunits)
			VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)
			ON CONFLICT(usage_date,api_key_id,provider_code,model) DO UPDATE SET
			requests=gateway.usage_daily.requests+EXCLUDED.requests,
			failed_requests=gateway.usage_daily.failed_requests+EXCLUDED.failed_requests,
			prompt_tokens=gateway.usage_daily.prompt_tokens+EXCLUDED.prompt_tokens,
			completion_tokens=gateway.usage_daily.completion_tokens+EXCLUDED.completion_tokens,
			cost_microunits=gateway.usage_daily.cost_microunits+EXCLUDED.cost_microunits,
			updated_at=clock_timestamp()`, key.date, key.apiKeyID, key.provider, key.model, value.requests, value.failed, value.prompt, value.completion, value.cost)
	}
	for _, alert := range alerts {
		batch.Queue(`INSERT INTO gateway.outbox_events(event_id,event_type,event_version,tenant_id,aggregate_type,aggregate_id,payload)
			VALUES($1,'content_policy.matched',1,$2,'gateway_request',$3,$4)`, alert.eventID, uuidPointer(alert.tenantID), alert.requestID, alert.payload)
	}
	results := tx.SendBatch(ctx, batch)
	if err := results.Close(); err != nil {
		return r.flushError(err)
	}
	if err := tx.Commit(ctx); err != nil {
		return r.flushError(err)
	}
	r.written.Add(uint64(len(events)))
	return nil
}

func (r *Recorder) flushError(err error) error {
	r.failures.Add(1)
	if r.logger != nil {
		r.logger.Error("audit batch flush failed", "error", err)
	}
	return err
}

func uuidValue(value string) pgtype.UUID {
	var result pgtype.UUID
	_ = result.Scan(value)
	return result
}

func uuidPointer(value *string) any {
	if value == nil || strings.TrimSpace(*value) == "" {
		return nil
	}
	return uuidValue(*value)
}

func nullString(value string) any {
	if value == "" {
		return nil
	}
	return value
}

func nullToken(value int64) any {
	if value <= 0 {
		return nil
	}
	return value
}

func nullCost(value int64) any {
	if value <= 0 {
		return nil
	}
	return value
}
