package agent

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"

	"github.com/jmoiron/sqlx"

	"github.com/tidbcloud/alertstore/pkg/subscription"
)

// ErrRunNotFound is returned when no run matches the identifier given.
var ErrRunNotFound = errors.New("agent run not found")

// SubscriptionStore reads the agent routing table.
type SubscriptionStore struct {
	db     *sqlx.DB
	logger *slog.Logger
}

func NewSubscriptionStore(db *sqlx.DB, logger *slog.Logger) *SubscriptionStore {
	return &SubscriptionStore{db: db, logger: logger}
}

func (s *SubscriptionStore) List(ctx context.Context) ([]Subscription, error) {
	const q = `SELECT * FROM agent_subscription ORDER BY name`
	var subs []Subscription
	if err := s.db.SelectContext(ctx, &subs, q); err != nil {
		return nil, fmt.Errorf("list agent subscriptions: %w", err)
	}
	return subs, nil
}

// FindMatching returns the enabled agents whose filter matches the alert.
//
// A filter that will not parse is skipped, not treated as matching everything.
// The same call in pkg/subscription reasons the same way and for the same
// reason: an unreadable filter is a broken row, and the failure that costs
// least is the one where a single agent goes quiet rather than every agent
// being dispatched every alert.
func (s *SubscriptionStore) FindMatching(ctx context.Context, t subscription.Target) ([]Subscription, error) {
	all, err := s.List(ctx)
	if err != nil {
		return nil, err
	}

	var matched []Subscription
	for _, sub := range all {
		if !sub.Enabled {
			continue
		}
		var f subscription.SubscriptionFilter
		if err := json.Unmarshal(sub.Filter, &f); err != nil {
			s.logger.Error("skip agent subscription with unreadable filter",
				"err", err, "agent", sub.Name, "id", sub.ID)
			continue
		}
		// An empty filter matches every owned alert, and until these rows are
		// written through a validating API they are written by hand — where the
		// commonest way to arrive here is a misspelled field name, which the
		// decoder drops in silence. Subscribing an agent to the entire fleet is
		// a thing somebody may genuinely want; it is not a thing a typo should
		// be able to do quietly.
		if f.IsEmpty() {
			s.logger.Error("skip agent subscription whose filter matches everything",
				"agent", sub.Name, "id", sub.ID, "filter", string(sub.Filter))
			continue
		}
		if f.Matches(t) {
			matched = append(matched, sub)
		}
	}
	return matched, nil
}

// RunStore records agent work against alerts.
type RunStore struct {
	db *sqlx.DB
}

func NewRunStore(db *sqlx.DB) *RunStore {
	return &RunStore{db: db}
}

// EnsureRun creates the run for a dispatch, or returns the existing one.
//
// Called from the delivery worker, which retries: (alert, agent, dedup key) is
// unique, so a redelivered dispatch re-posts to the same run rather than
// starting a second one and doubling the agent's work.
func (s *RunStore) EnsureRun(ctx context.Context, alertID, agentName, triggerKind, dedupKey string, parentRunID *int64, disclosure string) (*Run, error) {
	if existing, err := s.find(ctx, alertID, agentName, dedupKey); err != nil {
		return nil, err
	} else if existing != nil {
		return existing, nil
	}

	if disclosure == "" {
		disclosure = DisclosureShown
	}

	const q = `
		INSERT INTO agent_run (alert_id, agent, trigger_kind, dedup_key, parent_run_id, disclosure)
		VALUES (?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE id = id
	`
	if _, err := s.db.ExecContext(ctx, q,
		alertID, agentName, triggerKind, dedupKey, parentRunID, disclosure); err != nil {
		return nil, fmt.Errorf("create agent run %s/%s: %w", alertID, agentName, err)
	}

	// Re-read rather than trusting LastInsertId: the insert above is a no-op
	// when another worker won the race, and that returns 0.
	run, err := s.find(ctx, alertID, agentName, dedupKey)
	if err != nil {
		return nil, err
	}
	if run == nil {
		return nil, fmt.Errorf("agent run %s/%s vanished after insert", alertID, agentName)
	}
	return run, nil
}

func (s *RunStore) find(ctx context.Context, alertID, agentName, dedupKey string) (*Run, error) {
	const q = `SELECT * FROM agent_run WHERE alert_id = ? AND agent = ? AND dedup_key = ?`
	var run Run
	err := s.db.GetContext(ctx, &run, q, alertID, agentName, dedupKey)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("read agent run %s/%s: %w", alertID, agentName, err)
	}
	return &run, nil
}

// GetByID reads one run. The alert ID is part of the lookup, so a run that
// belongs to a different alert reads as missing rather than as somebody else's.
func (s *RunStore) GetByID(ctx context.Context, alertID string, runID int64) (*Run, error) {
	const q = `SELECT * FROM agent_run WHERE id = ? AND alert_id = ?`
	var run Run
	err := s.db.GetContext(ctx, &run, q, runID, alertID)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrRunNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("get agent run %d: %w", runID, err)
	}
	return &run, nil
}

func (s *RunStore) ListByAlertID(ctx context.Context, alertID string) ([]Run, error) {
	const q = `SELECT * FROM agent_run WHERE alert_id = ? ORDER BY started_at ASC, id ASC`
	var runs []Run
	if err := s.db.SelectContext(ctx, &runs, q, alertID); err != nil {
		return nil, fmt.Errorf("list agent runs for %s: %w", alertID, err)
	}
	return runs, nil
}

// Report is what an agent writes back. Every field is optional except Status:
// a run may report progress before it has a verdict, and a failed one has none
// at all.
type Report struct {
	Status     string
	Verdict    string
	Summary    string
	SessionRef string
	Detail     json.RawMessage
}

// Update applies an agent's report.
//
// Terminal is terminal: once a run has succeeded or failed, a later report
// cannot pull it back to running. Agents retry, and an HTTP retry that arrives
// out of order would otherwise leave a row claiming to be in progress with a
// finish time already on it, which reads as a run that never ends. For the same
// reason finished_at is stamped once rather than on every terminal report — a
// redelivered success must not stretch the duration every scorecard measures.
//
// The affected-row count is deliberately not consulted. The driver reports rows
// *changed*, not matched, so an agent re-sending an identical heartbeat changes
// nothing and would be told its run does not exist. Existence is answered by
// the read below, which is the question that was actually being asked.
func (s *RunStore) Update(ctx context.Context, alertID string, runID int64, rep Report) (*Run, error) {
	var detail interface{}
	if len(rep.Detail) > 0 {
		detail = []byte(rep.Detail)
	}

	// COALESCE on the optional fields so a progress report that carries only a
	// status cannot blank a summary an earlier one already wrote.
	const q = `
		UPDATE agent_run
		SET status      = CASE WHEN status IN ('succeeded', 'failed') THEN status ELSE ? END,
		    verdict     = COALESCE(NULLIF(?, ''), verdict),
		    summary     = COALESCE(NULLIF(?, ''), summary),
		    session_ref = COALESCE(NULLIF(?, ''), session_ref),
		    detail      = COALESCE(?, detail),
		    finished_at = CASE
		                    WHEN finished_at IS NULL AND ? IN ('succeeded', 'failed') THEN NOW(3)
		                    ELSE finished_at
		                  END
		WHERE id = ? AND alert_id = ?
	`
	if _, err := s.db.ExecContext(ctx, q,
		rep.Status, rep.Verdict, rep.Summary, rep.SessionRef, detail, rep.Status, runID, alertID); err != nil {
		return nil, fmt.Errorf("update agent run %d: %w", runID, err)
	}
	return s.GetByID(ctx, alertID, runID)
}
