package agent

import (
	"context"
	"fmt"
	"time"

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

// Stats is the Dashboards page's Agent section: aggregated in SQL over
// agent_run joined to alert, windowed on alert.created_at like the rest of
// the page.
type Stats struct {
	Alerts   int           `json:"alerts" validate:"required"`
	Covered  int           `json:"covered" validate:"required"` // alerts with at least one run
	Coverage []DayCoverage `json:"coverage" validate:"required"`
	Runs     RunCounts     `json:"runs" validate:"required"`
	// One row per verdict, always all five in vocabulary order; the last
	// verdict-bearing run per alert counts.
	Verdicts []VerdictOutcome `json:"verdicts" validate:"required"`
	// Share of diagnoses (noise / self_recovered / escalation) matching the
	// resolution a human recorded. Nil when nothing was scoreable; mitigated,
	// unknown and expired never are.
	AgreementRate  *float64 `json:"agreement_rate"`
	AgreementTotal int      `json:"agreement_total" validate:"required"`
}

type DayCoverage struct {
	Day     string `json:"day" validate:"required"`
	Alerts  int    `json:"alerts" validate:"required"`
	Covered int    `json:"covered" validate:"required"`
}

type RunCounts struct {
	Dispatched int `json:"dispatched" validate:"required"`
	Running    int `json:"running" validate:"required"`
	Succeeded  int `json:"succeeded" validate:"required"`
	Failed     int `json:"failed" validate:"required"`
}

// VerdictOutcome is what humans recorded on the alerts carrying one verdict;
// Open is the ones not closed yet.
type VerdictOutcome struct {
	Verdict       string `json:"verdict" validate:"required"`
	Handled       int    `json:"handled" validate:"required"`
	SelfRecovered int    `json:"self_recovered" validate:"required"`
	Noise         int    `json:"noise" validate:"required"`
	Expired       int    `json:"expired" validate:"required"`
	Open          int    `json:"open" validate:"required"`
	Total         int    `json:"total" validate:"required"`
}

var verdictOrder = []string{VerdictNoise, VerdictSelfRecovered, VerdictMitigated, VerdictEscalation, VerdictUnknown}

var expectedResolution = map[string]string{
	VerdictNoise:         alert.ResolutionNoise,
	VerdictSelfRecovered: alert.ResolutionSelfRecovered,
	VerdictEscalation:    alert.ResolutionHandled,
}

func (s *RunStore) Stats(ctx context.Context, from, to time.Time, team string) (*Stats, error) {
	coverage, err := s.coverage(ctx, from, to, team)
	if err != nil {
		return nil, err
	}
	runRows, err := s.runCounts(ctx, from, to, team)
	if err != nil {
		return nil, err
	}
	verdictRows, err := s.verdictOutcomes(ctx, from, to, team)
	if err != nil {
		return nil, err
	}

	st := &Stats{Coverage: coverage, Runs: pivotRunRows(runRows)}
	for _, d := range coverage {
		st.Alerts += d.Alerts
		st.Covered += d.Covered
	}
	st.Verdicts, st.AgreementRate, st.AgreementTotal = pivotVerdictRows(verdictRows)
	return st, nil
}

// COUNT(DISTINCT a.id): the LEFT JOIN repeats an alert once per run.
func (s *RunStore) coverage(ctx context.Context, from, to time.Time, team string) ([]DayCoverage, error) {
	q, args := alert.AppendTeamFilter(
		`SELECT DATE(DATE_ADD(a.created_at, INTERVAL 8 HOUR)) AS day,
		        COUNT(DISTINCT a.id) AS alerts,
		        COUNT(DISTINCT r.alert_id) AS covered
		 FROM alert a LEFT JOIN agent_run r ON r.alert_id = a.id
		 WHERE a.created_at > ? AND a.created_at <= ?`,
		[]any{from, to},
		team,
	)
	q += " GROUP BY day ORDER BY day"
	rows := []DayCoverage{}
	if err := s.db.SelectContext(ctx, &rows, q, args...); err != nil {
		return nil, fmt.Errorf("agent coverage: %w", err)
	}
	return rows, nil
}

type runRow struct {
	Status string `db:"status"`
	Count  int    `db:"count"`
}

func (s *RunStore) runCounts(ctx context.Context, from, to time.Time, team string) ([]runRow, error) {
	q, args := alert.AppendTeamFilter(
		`SELECT r.status, COUNT(*) AS count
		 FROM agent_run r JOIN alert a ON a.id = r.alert_id
		 WHERE a.created_at > ? AND a.created_at <= ?`,
		[]any{from, to},
		team,
	)
	q += " GROUP BY r.status"
	var rows []runRow
	if err := s.db.SelectContext(ctx, &rows, q, args...); err != nil {
		return nil, fmt.Errorf("agent run counts: %w", err)
	}
	return rows, nil
}

func pivotRunRows(rows []runRow) RunCounts {
	var c RunCounts
	for _, r := range rows {
		switch r.Status {
		case StatusDispatched:
			c.Dispatched += r.Count
		case StatusRunning:
			c.Running += r.Count
		case StatusSucceeded:
			c.Succeeded += r.Count
		case StatusFailed:
			c.Failed += r.Count
		}
	}
	return c
}

type verdictRow struct {
	Verdict    string `db:"verdict"`
	State      string `db:"state"`
	Resolution string `db:"resolution"`
	Count      int    `db:"count"`
}

func (s *RunStore) verdictOutcomes(ctx context.Context, from, to time.Time, team string) ([]verdictRow, error) {
	q, args := alert.AppendTeamFilter(
		`SELECT r.verdict, a.state, a.resolution, COUNT(*) AS count
		 FROM alert a JOIN agent_run r ON r.alert_id = a.id
		 WHERE r.id = (SELECT MAX(id) FROM agent_run WHERE alert_id = a.id AND verdict <> '')
		   AND a.created_at > ? AND a.created_at <= ?`,
		[]any{from, to},
		team,
	)
	q += " GROUP BY r.verdict, a.state, a.resolution"
	var rows []verdictRow
	if err := s.db.SelectContext(ctx, &rows, q, args...); err != nil {
		return nil, fmt.Errorf("agent verdict outcomes: %w", err)
	}
	return rows, nil
}

// A verdict outside the vocabulary (pre-000008 spelling) is skipped.
func pivotVerdictRows(rows []verdictRow) (outcomes []VerdictOutcome, rate *float64, scoreable int) {
	byVerdict := make(map[string]*VerdictOutcome, len(verdictOrder))
	outcomes = make([]VerdictOutcome, len(verdictOrder))
	for i, v := range verdictOrder {
		outcomes[i].Verdict = v
		byVerdict[v] = &outcomes[i]
	}

	agree := 0
	for _, r := range rows {
		row, ok := byVerdict[r.Verdict]
		if !ok {
			continue
		}
		outcome := r.Resolution
		if r.State != alert.StateResolved {
			outcome = ""
		}
		switch outcome {
		case alert.ResolutionHandled:
			row.Handled += r.Count
		case alert.ResolutionSelfRecovered:
			row.SelfRecovered += r.Count
		case alert.ResolutionNoise:
			row.Noise += r.Count
		case alert.ResolutionExpired:
			row.Expired += r.Count
		default:
			row.Open += r.Count
		}
		row.Total += r.Count

		if want, diagnosis := expectedResolution[r.Verdict]; diagnosis && outcome != "" && outcome != alert.ResolutionExpired {
			scoreable += r.Count
			if outcome == want {
				agree += r.Count
			}
		}
	}

	if scoreable > 0 {
		v := float64(agree) / float64(scoreable)
		rate = &v
	}
	return outcomes, rate, scoreable
}
