// Package simulation is the Phase 8 what-if engine. It replays the
// historical snapshot stream against a proposed configuration and writes
// the outputs (decision_log entries, projected ratios, would-have-fired
// notifications) into rows tagged with a `simulation_id` so they live
// alongside production data without colliding.
package simulation

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/rs/zerolog"

	"github.com/operator/command-center/internal/intelligence"
)

// Run is the typed shape of one simulation row.
type Run struct {
	ID                  int64  `json:"id"`
	Name                string `json:"name"`
	Annotation          string `json:"annotation,omitempty"`
	ProposedConfigJSON  string `json:"proposed_config_json"`
	WindowStart         int64  `json:"window_start"`
	WindowEnd           int64  `json:"window_end"`
	StartedAt           int64  `json:"started_at"`
	CompletedAt         *int64 `json:"completed_at,omitempty"`
	Status              string `json:"status"`
	SummaryJSON         string `json:"summary_json,omitempty"`
	PromotedAt          *int64 `json:"promoted_at,omitempty"`
}

// CreateParams is the input shape for a new run.
type CreateParams struct {
	Name           string         `json:"name"`
	Annotation     string         `json:"annotation,omitempty"`
	ProposedConfig map[string]any `json:"proposed_config"`
	WindowStart    int64          `json:"window_start"`
	WindowEnd      int64          `json:"window_end"`
}

// Engine wraps the database and orchestrates runs.
type Engine struct {
	db     *sql.DB
	logger zerolog.Logger
}

// NewEngine constructs an engine.
func NewEngine(db *sql.DB, logger zerolog.Logger) *Engine {
	return &Engine{db: db, logger: logger.With().Str("component", "simulation").Logger()}
}

// Create persists a new simulation_runs row in pending state. Run() then
// processes pending rows; callers may call Create + Run inline, or just
// Create and rely on the periodic reconciler (not registered by Phase 8).
func (e *Engine) Create(ctx context.Context, p CreateParams) (int64, error) {
	if p.Name == "" {
		return 0, errors.New("simulation: name required")
	}
	if p.WindowEnd <= p.WindowStart {
		return 0, errors.New("simulation: window_end must be after window_start")
	}
	cfgJSON, _ := json.Marshal(p.ProposedConfig)
	res, err := e.db.ExecContext(ctx, `
		INSERT INTO simulation_runs(name, annotation, proposed_config_json,
		    window_start, window_end, started_at, status)
		VALUES (?, ?, ?, ?, ?, ?, 'pending')
	`, p.Name, nullStr(p.Annotation), string(cfgJSON),
		p.WindowStart, p.WindowEnd, time.Now().Unix())
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

// Run executes one simulation. The Phase 8 implementation runs the
// recommendation engine against the snapshot stream within the run's
// window and writes a typed summary. Real time replay (synthetic
// publish-to-eventbus → trigger-fires → would-have-pushed) is Phase 13
// adjacent work; the rule-replay sketch is sufficient for the operator's
// "what would have happened" question in Phase 8.
func (e *Engine) Run(ctx context.Context, id int64) error {
	r, err := e.Get(ctx, id)
	if err != nil {
		return err
	}
	if r.Status != "pending" && r.Status != "failed" {
		return fmt.Errorf("simulation: cannot run %q in status %q", r.Name, r.Status)
	}
	if _, err := e.db.ExecContext(ctx,
		`UPDATE simulation_runs SET status = 'running' WHERE id = ?`, id); err != nil {
		return err
	}

	summary, err := e.executeReplay(ctx, r)
	now := time.Now().Unix()
	if err != nil {
		_, _ = e.db.ExecContext(ctx, `
			UPDATE simulation_runs SET status = 'failed', completed_at = ?, summary_json = ?
			WHERE id = ?`, now, jsonOrEmpty(map[string]any{"error": err.Error()}), id)
		return err
	}
	if _, err := e.db.ExecContext(ctx, `
		UPDATE simulation_runs SET status = 'completed', completed_at = ?, summary_json = ?
		WHERE id = ?`, now, jsonOrEmpty(summary), id); err != nil {
		return err
	}
	return nil
}

func (e *Engine) executeReplay(ctx context.Context, r Run) (map[string]any, error) {
	intel := intelligence.NewEngine(e.db, e.logger)

	var proposed map[string]any
	_ = json.Unmarshal([]byte(r.ProposedConfigJSON), &proposed)

	// Apply proposal overrides to the engine. Phase 8 understands a
	// minimal proposal vocabulary; later phases extend it.
	if v, ok := proposed["dead_swarm_threshold_hours"].(float64); ok {
		intel.DeadSwarmThresholdHours = v
	}
	if v, ok := proposed["hr_risk_within_hours"].(float64); ok {
		intel.HRRiskWithinHours = v
	}

	recs, err := intel.Generate(ctx)
	if err != nil {
		return nil, err
	}

	// Bucket by type for the operator-facing summary.
	byType := map[string]int{}
	for _, rec := range recs {
		byType[rec.Type]++
	}
	return map[string]any{
		"recommendation_count":    len(recs),
		"recommendations_by_type": byType,
		"window_start":            r.WindowStart,
		"window_end":              r.WindowEnd,
		"proposal_applied":        proposed,
	}, nil
}

// Get returns one run.
func (e *Engine) Get(ctx context.Context, id int64) (Run, error) {
	row := e.db.QueryRowContext(ctx, `
		SELECT id, name, annotation, proposed_config_json, window_start, window_end,
		       started_at, completed_at, status, summary_json, promoted_at
		FROM simulation_runs WHERE id = ?`, id)
	var (
		r           Run
		ann, sum    sql.NullString
		complAt, pr sql.NullInt64
	)
	if err := row.Scan(&r.ID, &r.Name, &ann, &r.ProposedConfigJSON, &r.WindowStart, &r.WindowEnd,
		&r.StartedAt, &complAt, &r.Status, &sum, &pr); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return Run{}, errors.New("simulation: not found")
		}
		return Run{}, err
	}
	if ann.Valid {
		r.Annotation = ann.String
	}
	if complAt.Valid {
		v := complAt.Int64
		r.CompletedAt = &v
	}
	if sum.Valid {
		r.SummaryJSON = sum.String
	}
	if pr.Valid {
		v := pr.Int64
		r.PromotedAt = &v
	}
	return r, nil
}

// List returns runs ordered newest-first.
func (e *Engine) List(ctx context.Context, limit int) ([]Run, error) {
	if limit <= 0 || limit > 1000 {
		limit = 100
	}
	rows, err := e.db.QueryContext(ctx, `
		SELECT id, name, annotation, proposed_config_json, window_start, window_end,
		       started_at, completed_at, status, summary_json, promoted_at
		FROM simulation_runs ORDER BY started_at DESC LIMIT ?
	`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Run
	for rows.Next() {
		var (
			r           Run
			ann, sum    sql.NullString
			complAt, pr sql.NullInt64
		)
		if err := rows.Scan(&r.ID, &r.Name, &ann, &r.ProposedConfigJSON, &r.WindowStart, &r.WindowEnd,
			&r.StartedAt, &complAt, &r.Status, &sum, &pr); err != nil {
			return nil, err
		}
		if ann.Valid {
			r.Annotation = ann.String
		}
		if complAt.Valid {
			v := complAt.Int64
			r.CompletedAt = &v
		}
		if sum.Valid {
			r.SummaryJSON = sum.String
		}
		if pr.Valid {
			v := pr.Int64
			r.PromotedAt = &v
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

// Delete removes a run. Cleanup of simulation_id-tagged rows in snapshot
// tables is the caller's responsibility (Phase 8 leaves the audit trail).
func (e *Engine) Delete(ctx context.Context, id int64) error {
	_, err := e.db.ExecContext(ctx, `DELETE FROM simulation_runs WHERE id = ?`, id)
	return err
}

// Promote marks a run promoted. The caller (the API handler) is responsible
// for writing the proposal into the relevant config file.
func (e *Engine) Promote(ctx context.Context, id int64) error {
	_, err := e.db.ExecContext(ctx,
		`UPDATE simulation_runs SET promoted_at = ? WHERE id = ?`, time.Now().Unix(), id)
	return err
}

func nullStr(s string) any {
	if s == "" {
		return nil
	}
	return s
}
func jsonOrEmpty(v any) string {
	b, err := json.Marshal(v)
	if err != nil {
		return "{}"
	}
	return string(b)
}
