package postgres

import (
	"context"
	"encoding/json"
	"time"

	"github.com/acp/interceptor/internal/approval"
	"github.com/acp/interceptor/internal/models"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

// ApprovalStore persists approval tickets in Postgres.
type ApprovalStore struct {
	pool *pgxpool.Pool
}

// NewApprovalStore creates a Postgres-backed approval repository.
func NewApprovalStore(pool *pgxpool.Pool) *ApprovalStore {
	return &ApprovalStore{pool: pool}
}

func (s *ApprovalStore) UpsertPending(traceID string, req models.ToolCallRequest, escalateReason, policy string) *approval.Record {
	now := time.Now().UTC()
	rec := &approval.Record{
		TraceID: traceID, Status: approval.StatusPending,
		AgentID: req.AgentID, Tool: req.Tool, Action: req.Action,
		Amount: req.Amount, RiskScore: req.RiskScore, Extra: req.Extra,
		EscalateReason: escalateReason, Policy: policy, CreatedAt: now,
	}
	extra, _ := json.Marshal(req.Extra)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, err := s.pool.Exec(ctx, `
		INSERT INTO approval_records (
			trace_id, status, agent_id, tool, action, amount, risk_score, extra,
			escalate_reason, policy, created_at
		) VALUES ($1,'pending',$2,$3,$4,$5,$6,$7,$8,$9,$10)
		ON CONFLICT (trace_id) DO UPDATE SET
			agent_id = EXCLUDED.agent_id,
			tool = EXCLUDED.tool,
			action = EXCLUDED.action,
			amount = EXCLUDED.amount,
			risk_score = EXCLUDED.risk_score,
			extra = EXCLUDED.extra,
			escalate_reason = EXCLUDED.escalate_reason,
			policy = EXCLUDED.policy
		WHERE approval_records.status = 'pending'`,
		traceID, rec.AgentID, rec.Tool, rec.Action, rec.Amount, rec.RiskScore, extra,
		escalateReason, policy, now,
	)
	if err != nil {
		return rec
	}
	if existing, ok := s.Get(traceID); ok {
		return existing
	}
	return rec
}

func (s *ApprovalStore) Get(traceID string) (*approval.Record, bool) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	row := s.pool.QueryRow(ctx, `
		SELECT trace_id, status, agent_id, tool, action, amount, risk_score, extra,
		       escalate_reason, policy, created_at, decided_at, decided_by
		FROM approval_records WHERE trace_id = $1`, traceID)
	rec, err := scanApproval(row)
	if err != nil {
		return nil, false
	}
	return rec, true
}

func (s *ApprovalStore) List(status approval.Status, limit int) []approval.Record {
	if limit < 1 {
		limit = 50
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	var rows pgx.Rows
	var err error
	if status == "" {
		rows, err = s.pool.Query(ctx, `
			SELECT trace_id, status, agent_id, tool, action, amount, risk_score, extra,
			       escalate_reason, policy, created_at, decided_at, decided_by
			FROM approval_records ORDER BY created_at DESC LIMIT $1`, limit)
	} else {
		rows, err = s.pool.Query(ctx, `
			SELECT trace_id, status, agent_id, tool, action, amount, risk_score, extra,
			       escalate_reason, policy, created_at, decided_at, decided_by
			FROM approval_records WHERE status = $1 ORDER BY created_at DESC LIMIT $2`,
			string(status), limit)
	}
	if err != nil {
		return nil
	}
	defer rows.Close()
	var out []approval.Record
	for rows.Next() {
		rec, err := scanApproval(rows)
		if err != nil {
			continue
		}
		out = append(out, *rec)
	}
	return out
}

func (s *ApprovalStore) Approve(traceID, decidedBy string) (*approval.Record, error) {
	return s.decide(traceID, approval.StatusApproved, decidedBy)
}

func (s *ApprovalStore) Deny(traceID, decidedBy string) (*approval.Record, error) {
	return s.decide(traceID, approval.StatusDenied, decidedBy)
}

func (s *ApprovalStore) decide(traceID string, status approval.Status, decidedBy string) (*approval.Record, error) {
	now := time.Now().UTC()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	tag, err := s.pool.Exec(ctx, `
		UPDATE approval_records
		SET status = $1, decided_at = $2, decided_by = $3
		WHERE trace_id = $4 AND status = 'pending'`,
		string(status), now, decidedBy, traceID,
	)
	if err != nil {
		return nil, err
	}
	if tag.RowsAffected() == 0 {
		rec, ok := s.Get(traceID)
		if !ok {
			return nil, approval.ErrNotFound
		}
		if rec.Status != approval.StatusPending {
			return nil, approval.ErrNotPending
		}
		return nil, approval.ErrNotFound
	}
	rec, ok := s.Get(traceID)
	if !ok {
		return nil, approval.ErrNotFound
	}
	return rec, nil
}

type rowScanner interface {
	Scan(dest ...any) error
}

func scanApproval(row rowScanner) (*approval.Record, error) {
	var rec approval.Record
	var extra []byte
	var status string
	var decidedAt *time.Time
	if err := row.Scan(
		&rec.TraceID, &status, &rec.AgentID, &rec.Tool, &rec.Action,
		&rec.Amount, &rec.RiskScore, &extra,
		&rec.EscalateReason, &rec.Policy, &rec.CreatedAt, &decidedAt, &rec.DecidedBy,
	); err != nil {
		return nil, err
	}
	rec.Status = approval.Status(status)
	rec.DecidedAt = decidedAt
	if len(extra) > 0 && string(extra) != "null" {
		_ = json.Unmarshal(extra, &rec.Extra)
	}
	return &rec, nil
}
