// Package budgets is the Phase 10 health-budget framework. Operators
// declare SLO-style tolerances in `config/health-budgets.yaml`; the tracker
// records consumption from typed events and from periodic metric queries,
// and the dispatcher's `health_budget_burn` trigger fires when burn rate
// exceeds the configured threshold.
package budgets

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/rs/zerolog"
)

// Period is one of "week" | "month" | "quarter".
type Period string

const (
	PeriodWeek    Period = "week"
	PeriodMonth   Period = "month"
	PeriodQuarter Period = "quarter"
)

// Budget is one operator-declared tolerance.
type Budget struct {
	Name     string  `yaml:"name"`
	Capacity float64 `yaml:"capacity"`
	Period   Period  `yaml:"period"`
	Unit     string  `yaml:"unit"`
	// ConsumedBy can be event-driven OR metric-driven.
	ConsumedBy ConsumedBy `yaml:"consumed_by"`
}

// ConsumedBy declares how the budget accumulates.
type ConsumedBy struct {
	EventType     string  `yaml:"event_type,omitempty"`
	PerEventCost  float64 `yaml:"per_event_cost,omitempty"`
	MetricQuery   string  `yaml:"metric_query,omitempty"`
}

// State is the current period's row.
type State struct {
	BudgetName    string  `json:"budget_name"`
	PeriodStart   int64   `json:"period_start"`
	PeriodEnd     int64   `json:"period_end"`
	Consumed      float64 `json:"consumed"`
	Capacity      float64 `json:"capacity"`
	BurnRate      float64 `json:"burn_rate"`
	LastUpdatedAt int64   `json:"last_updated_at"`
}

// Tracker wraps the health_budget_state table + an in-memory map of
// budget definitions.
type Tracker struct {
	db     *sql.DB
	logger zerolog.Logger
	mu     sync.RWMutex
	defs   map[string]Budget
}

// NewTracker constructs an empty tracker.
func NewTracker(db *sql.DB, logger zerolog.Logger) *Tracker {
	return &Tracker{db: db, logger: logger.With().Str("component", "budgets").Logger(), defs: map[string]Budget{}}
}

// Replace swaps the in-memory budget set (called on reload).
func (t *Tracker) Replace(bs []Budget) {
	t.mu.Lock()
	defer t.mu.Unlock()
	t.defs = map[string]Budget{}
	for _, b := range bs {
		t.defs[b.Name] = b
	}
}

// Defs returns a copy of the current definitions.
func (t *Tracker) Defs() []Budget {
	t.mu.RLock()
	defer t.mu.RUnlock()
	out := make([]Budget, 0, len(t.defs))
	for _, b := range t.defs {
		out = append(out, b)
	}
	return out
}

// Increment adds cost to budget b's current-period consumption. Used by
// event-driven budgets from Phase 4 webhook handlers / bus subscribers.
func (t *Tracker) Increment(ctx context.Context, name string, cost float64) error {
	t.mu.RLock()
	def, ok := t.defs[name]
	t.mu.RUnlock()
	if !ok {
		return fmt.Errorf("budgets: unknown budget %q", name)
	}
	start, end := periodBounds(def.Period, time.Now())
	now := time.Now().Unix()
	tx, err := t.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()
	_, err = tx.ExecContext(ctx, `
		INSERT INTO health_budget_state(budget_name, period_start, period_end, consumed, capacity, last_updated_at)
		VALUES (?, ?, ?, ?, ?, ?)
		ON CONFLICT(budget_name, period_start) DO UPDATE SET
		  consumed = consumed + excluded.consumed,
		  last_updated_at = excluded.last_updated_at
	`, name, start.Unix(), end.Unix(), cost, def.Capacity, now)
	if err != nil {
		return err
	}
	return tx.Commit()
}

// CurrentStates returns one State per known budget for the current period.
func (t *Tracker) CurrentStates(ctx context.Context) ([]State, error) {
	t.mu.RLock()
	defs := make([]Budget, 0, len(t.defs))
	for _, b := range t.defs {
		defs = append(defs, b)
	}
	t.mu.RUnlock()
	now := time.Now()
	out := []State{}
	for _, def := range defs {
		start, end := periodBounds(def.Period, now)
		row := t.db.QueryRowContext(ctx, `
			SELECT consumed, capacity, last_updated_at FROM health_budget_state
			WHERE budget_name = ? AND period_start = ?
		`, def.Name, start.Unix())
		var (
			consumed   sql.NullFloat64
			capacity   sql.NullFloat64
			lastUpdate sql.NullInt64
		)
		_ = row.Scan(&consumed, &capacity, &lastUpdate)
		state := State{
			BudgetName:  def.Name,
			PeriodStart: start.Unix(),
			PeriodEnd:   end.Unix(),
			Consumed:    consumed.Float64,
			Capacity:    def.Capacity,
		}
		if capacity.Valid {
			state.Capacity = capacity.Float64
		}
		if lastUpdate.Valid {
			state.LastUpdatedAt = lastUpdate.Int64
		}
		// burn_rate = (consumed/capacity) / (elapsed/period_length)
		elapsed := now.Sub(start).Seconds()
		periodLen := end.Sub(start).Seconds()
		if state.Capacity > 0 && elapsed > 0 && periodLen > 0 {
			state.BurnRate = (state.Consumed / state.Capacity) / (elapsed / periodLen)
		}
		out = append(out, state)
	}
	return out, nil
}

// History returns past-period final states for one budget.
func (t *Tracker) History(ctx context.Context, name string, limit int) ([]State, error) {
	if limit <= 0 || limit > 100 {
		limit = 12
	}
	rows, err := t.db.QueryContext(ctx, `
		SELECT period_start, period_end, consumed, capacity, last_updated_at
		FROM health_budget_state WHERE budget_name = ?
		ORDER BY period_start DESC LIMIT ?
	`, name, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []State{}
	for rows.Next() {
		s := State{BudgetName: name}
		if err := rows.Scan(&s.PeriodStart, &s.PeriodEnd, &s.Consumed, &s.Capacity, &s.LastUpdatedAt); err != nil {
			return nil, err
		}
		out = append(out, s)
	}
	return out, rows.Err()
}

// periodBounds returns the [start, end) of the current period containing now.
// Aligned to UTC midnight; week starts Monday.
func periodBounds(p Period, now time.Time) (time.Time, time.Time) {
	u := now.UTC()
	switch p {
	case PeriodWeek:
		// Start of the ISO week.
		wd := int(u.Weekday())
		if wd == 0 {
			wd = 7
		}
		start := time.Date(u.Year(), u.Month(), u.Day()-(wd-1), 0, 0, 0, 0, time.UTC)
		return start, start.Add(7 * 24 * time.Hour)
	case PeriodQuarter:
		q := (int(u.Month()) - 1) / 3
		startMonth := time.Month(q*3 + 1)
		start := time.Date(u.Year(), startMonth, 1, 0, 0, 0, 0, time.UTC)
		end := start.AddDate(0, 3, 0)
		return start, end
	default: // month
		start := time.Date(u.Year(), u.Month(), 1, 0, 0, 0, 0, time.UTC)
		end := start.AddDate(0, 1, 0)
		return start, end
	}
}

// MetricReconciler runs metric-driven budget evaluation on the Phase 4
// reconcile loop. Each cycle it queries the DB per budget that uses
// `metric_query` and updates the row accordingly.
type MetricReconciler struct {
	tracker *Tracker
}

// NewMetricReconciler constructs the reconciler.
func NewMetricReconciler(t *Tracker) *MetricReconciler {
	return &MetricReconciler{tracker: t}
}

// Name satisfies scrape.Reconciler.
func (r *MetricReconciler) Name() string { return "budget-metrics" }

// Reconcile runs one pass.
func (r *MetricReconciler) Reconcile(ctx context.Context) error {
	defs := r.tracker.Defs()
	for _, def := range defs {
		if def.ConsumedBy.MetricQuery == "" {
			continue
		}
		// Operator's query is expected to return a single numeric value
		// for the current cumulative consumption. We REPLACE rather than
		// increment for metric-driven budgets.
		var v sql.NullFloat64
		row := r.tracker.db.QueryRowContext(ctx, def.ConsumedBy.MetricQuery)
		if err := row.Scan(&v); err != nil {
			r.tracker.logger.Warn().Err(err).Str("budget", def.Name).Msg("metric query failed")
			continue
		}
		start, end := periodBounds(def.Period, time.Now())
		_, err := r.tracker.db.ExecContext(ctx, `
			INSERT INTO health_budget_state(budget_name, period_start, period_end, consumed, capacity, last_updated_at)
			VALUES (?, ?, ?, ?, ?, ?)
			ON CONFLICT(budget_name, period_start) DO UPDATE SET
			  consumed = excluded.consumed,
			  last_updated_at = excluded.last_updated_at
		`, def.Name, start.Unix(), end.Unix(), v.Float64, def.Capacity, time.Now().Unix())
		if err != nil {
			return err
		}
	}
	return nil
}

// Errors that callers may want to inspect.
var (
	ErrUnknownBudget = errors.New("budgets: unknown budget")
)
