package approval

import (
	"reflect"
	"sync"
	"time"

	"github.com/acp/interceptor/internal/models"
)

// Status is the human decision state for a trace-bound escalation.
type Status string

const (
	StatusPending  Status = "pending"
	StatusApproved Status = "approved"
	StatusDenied   Status = "denied"
)

// Record is one escalation ticket keyed by trace_id.
type Record struct {
	TraceID        string         `json:"trace_id"`
	Status         Status         `json:"status"`
	AgentID        string         `json:"agent_id"`
	Tool           string         `json:"tool"`
	Action         string         `json:"action"`
	Amount         *int64         `json:"amount,omitempty"`
	RiskScore      *float64       `json:"risk_score,omitempty"`
	Extra          map[string]any `json:"extra,omitempty"`
	EscalateReason string         `json:"escalate_reason,omitempty"`
	Policy         string         `json:"policy,omitempty"`
	CreatedAt      time.Time      `json:"created_at"`
	DecidedAt      *time.Time     `json:"decided_at,omitempty"`
	DecidedBy      string         `json:"decided_by,omitempty"`
}

// Store holds pending/approved/denied records in memory (dashboard + retry contract).
type Store struct {
	mu      sync.RWMutex
	byTrace map[string]*Record
	order   []string
	cap     int
}

// NewStore creates a bounded in-memory approval store.
func NewStore(capacity int) *Store {
	if capacity < 1 {
		capacity = 200
	}
	return &Store{
		byTrace: make(map[string]*Record),
		cap:     capacity,
	}
}

// UpsertPending creates or refreshes a pending ticket for trace_id (does not clobber decided rows).
func (s *Store) UpsertPending(traceID string, req models.ToolCallRequest, escalateReason, policy string) *Record {
	now := time.Now().UTC()
	rec := &Record{
		TraceID:        traceID,
		Status:         StatusPending,
		AgentID:        req.AgentID,
		Tool:           req.Tool,
		Action:         req.Action,
		Amount:         cloneInt64(req.Amount),
		RiskScore:      cloneFloat64(req.RiskScore),
		Extra:          cloneExtra(req.Extra),
		EscalateReason: escalateReason,
		Policy:         policy,
		CreatedAt:      now,
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	if existing, ok := s.byTrace[traceID]; ok {
		if existing.Status != StatusPending {
			return existing
		}
		rec.CreatedAt = existing.CreatedAt
		s.byTrace[traceID] = rec
		return rec
	}

	s.byTrace[traceID] = rec
	s.order = append([]string{traceID}, s.order...)
	if len(s.order) > s.cap {
		oldest := s.order[len(s.order)-1]
		s.order = s.order[:len(s.order)-1]
		delete(s.byTrace, oldest)
	}
	return rec
}

// Get returns a record by trace_id.
func (s *Store) Get(traceID string) (*Record, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	rec, ok := s.byTrace[traceID]
	if !ok {
		return nil, false
	}
	cp := *rec
	return &cp, true
}

// List returns records filtered by status (empty status = all), newest first.
func (s *Store) List(status Status, limit int) []Record {
	if limit < 1 {
		limit = 50
	}
	s.mu.RLock()
	defer s.mu.RUnlock()

	out := make([]Record, 0, limit)
	for _, id := range s.order {
		rec, ok := s.byTrace[id]
		if !ok {
			continue
		}
		if status != "" && rec.Status != status {
			continue
		}
		cp := *rec
		out = append(out, cp)
		if len(out) >= limit {
			break
		}
	}
	return out
}

// Approve marks a pending record approved.
func (s *Store) Approve(traceID, decidedBy string) (*Record, error) {
	return s.decide(traceID, StatusApproved, decidedBy)
}

// Deny marks a pending record denied.
func (s *Store) Deny(traceID, decidedBy string) (*Record, error) {
	return s.decide(traceID, StatusDenied, decidedBy)
}

func (s *Store) decide(traceID string, status Status, decidedBy string) (*Record, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	rec, ok := s.byTrace[traceID]
	if !ok {
		return nil, ErrNotFound
	}
	if rec.Status != StatusPending {
		return nil, ErrNotPending
	}
	now := time.Now().UTC()
	rec.Status = status
	rec.DecidedAt = &now
	rec.DecidedBy = decidedBy
	cp := *rec
	return &cp, nil
}

// MatchesRequest reports whether the tool-call body is the same invocation the ticket was opened for.
func (r *Record) MatchesRequest(req models.ToolCallRequest) bool {
	if r.AgentID != req.AgentID || r.Tool != req.Tool || r.Action != req.Action {
		return false
	}
	if !ptrInt64Equal(r.Amount, req.Amount) {
		return false
	}
	if !ptrFloat64Equal(r.RiskScore, req.RiskScore) {
		return false
	}
	return reflect.DeepEqual(r.Extra, req.Extra)
}

func ptrInt64Equal(a, b *int64) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

func ptrFloat64Equal(a, b *float64) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

func cloneInt64(p *int64) *int64 {
	if p == nil {
		return nil
	}
	v := *p
	return &v
}

func cloneFloat64(p *float64) *float64 {
	if p == nil {
		return nil
	}
	v := *p
	return &v
}

func cloneExtra(m map[string]any) map[string]any {
	if m == nil {
		return nil
	}
	out := make(map[string]any, len(m))
	for k, v := range m {
		out[k] = v
	}
	return out
}
