package store

import (
	"database/sql"
	"errors"
	"fmt"
	"strings"
)

// CreateLogScanRule inserts a new rule row. Validates severity +
// streams enum membership and rejects negative cooldowns.
func (s *Store) CreateLogScanRule(r LogScanRule) (LogScanRule, error) {
	if err := validateLogScanRule(r); err != nil {
		return LogScanRule{}, err
	}
	now := Now()
	r.CreatedAt = now
	r.UpdatedAt = now
	res, err := s.db.Exec(
		`INSERT INTO log_scan_rules
		   (workload_id, overrides_id, name, pattern, severity, streams,
		    cooldown_seconds, enabled, created_at, updated_at)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		r.WorkloadID, r.OverridesID, r.Name, r.Pattern, r.Severity, r.Streams,
		r.CooldownSeconds, boolToInt(r.Enabled), r.CreatedAt, r.UpdatedAt,
	)
	if err != nil {
		return LogScanRule{}, fmt.Errorf("insert log scan rule: %w", err)
	}
	id, err := res.LastInsertId()
	if err != nil {
		return LogScanRule{}, fmt.Errorf("get log scan rule id: %w", err)
	}
	r.ID = id
	return r, nil
}

// ListLogScanRules returns every rule, ordered by id for stable UI
// rendering.
func (s *Store) ListLogScanRules() ([]LogScanRule, error) {
	return s.queryLogScanRules(
		`SELECT id, workload_id, overrides_id, name, pattern, severity, streams,
		        cooldown_seconds, enabled, created_at, updated_at
		 FROM log_scan_rules ORDER BY id`,
	)
}

// ListLogScanRulesByWorkload returns all rows directly attached to
// the workload (workload-only additions and per-workload overrides),
// excluding global rules. Useful for the workload detail page.
func (s *Store) ListLogScanRulesByWorkload(workloadID string) ([]LogScanRule, error) {
	return s.queryLogScanRules(
		`SELECT id, workload_id, overrides_id, name, pattern, severity, streams,
		        cooldown_seconds, enabled, created_at, updated_at
		 FROM log_scan_rules WHERE workload_id = ? ORDER BY id`,
		workloadID,
	)
}

// GetLogScanRule fetches one rule by id or returns ErrNotFound.
func (s *Store) GetLogScanRule(id int64) (LogScanRule, error) {
	row := s.db.QueryRow(
		`SELECT id, workload_id, overrides_id, name, pattern, severity, streams,
		        cooldown_seconds, enabled, created_at, updated_at
		 FROM log_scan_rules WHERE id = ?`, id,
	)
	r, err := scanLogScanRuleRow(row)
	if errors.Is(err, sql.ErrNoRows) {
		return LogScanRule{}, fmt.Errorf("log scan rule %d: %w", id, ErrNotFound)
	}
	if err != nil {
		return LogScanRule{}, fmt.Errorf("query log scan rule: %w", err)
	}
	return r, nil
}

// UpdateLogScanRule overwrites the editable columns of a rule row.
// id, workload_id, overrides_id are immutable on update — change the
// scope of a rule by deleting + recreating, to keep the
// hot-reload-snapshot semantics simple.
func (s *Store) UpdateLogScanRule(r LogScanRule) (LogScanRule, error) {
	if r.ID == 0 {
		return LogScanRule{}, fmt.Errorf("log scan rule: id is required for update")
	}
	if err := validateLogScanRule(r); err != nil {
		return LogScanRule{}, err
	}
	r.UpdatedAt = Now()
	res, err := s.db.Exec(
		`UPDATE log_scan_rules
		    SET name = ?, pattern = ?, severity = ?, streams = ?,
		        cooldown_seconds = ?, enabled = ?, updated_at = ?
		  WHERE id = ?`,
		r.Name, r.Pattern, r.Severity, r.Streams,
		r.CooldownSeconds, boolToInt(r.Enabled), r.UpdatedAt, r.ID,
	)
	if err != nil {
		return LogScanRule{}, fmt.Errorf("update log scan rule: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return LogScanRule{}, fmt.Errorf("log scan rule %d: %w", r.ID, ErrNotFound)
	}
	return s.GetLogScanRule(r.ID)
}

// DeleteLogScanRule removes a rule by id. Override rows referencing
// this id are cascade-deleted at the application layer because we
// don't enforce SQLite FK constraints repo-wide. The two DELETEs run
// inside a single transaction so a mid-cascade failure can't leave
// overrides orphaned by a vanished global.
func (s *Store) DeleteLogScanRule(id int64) error {
	tx, err := s.db.Begin()
	if err != nil {
		return fmt.Errorf("begin delete tx: %w", err)
	}
	defer tx.Rollback() //nolint:errcheck // commit path returns nil; rollback after commit is a no-op
	if _, err := tx.Exec(`DELETE FROM log_scan_rules WHERE overrides_id = ?`, id); err != nil {
		return fmt.Errorf("delete dependent log scan overrides: %w", err)
	}
	res, err := tx.Exec(`DELETE FROM log_scan_rules WHERE id = ?`, id)
	if err != nil {
		return fmt.Errorf("delete log scan rule: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return fmt.Errorf("log scan rule %d: %w", id, ErrNotFound)
	}
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("commit delete tx: %w", err)
	}
	return nil
}

// EffectiveLogScanRules computes the effective rule set for one
// workload according to the spec in docs/LOGSCAN_AND_TRIGGERS_TODO.md:
//
//  1. All global rules (workload_id == "" AND overrides_id == 0)
//     minus globals that have a per-workload override row.
//  2. Plus workload-only rules (workload_id == X AND overrides_id == 0).
//  3. Plus per-workload override rules (workload_id == X AND overrides_id != 0),
//     which carry the override's own enabled/pattern/severity.
//
// Computed in Go after two simple SELECTs since rule counts will be
// small (operator-curated, dozens not thousands).
func (s *Store) EffectiveLogScanRules(workloadID string) ([]LogScanRule, error) {
	all, err := s.ListLogScanRules()
	if err != nil {
		return nil, err
	}
	overrides := map[int64]LogScanRule{} // globalID -> override row
	var workloadOnly []LogScanRule
	var globals []LogScanRule
	for _, r := range all {
		switch {
		case r.WorkloadID == "" && r.OverridesID == 0:
			globals = append(globals, r)
		case r.WorkloadID == workloadID && r.OverridesID == 0:
			workloadOnly = append(workloadOnly, r)
		case r.WorkloadID == workloadID && r.OverridesID != 0:
			overrides[r.OverridesID] = r
		}
	}
	out := make([]LogScanRule, 0, len(globals)+len(workloadOnly))
	for _, g := range globals {
		if ov, ok := overrides[g.ID]; ok {
			// Override row's fields win — including enabled=false to
			// turn off the global for this workload.
			out = append(out, ov)
		} else {
			out = append(out, g)
		}
	}
	out = append(out, workloadOnly...)
	return out, nil
}

func (s *Store) queryLogScanRules(query string, args ...any) ([]LogScanRule, error) {
	rows, err := s.db.Query(query, args...)
	if err != nil {
		return nil, fmt.Errorf("query log scan rules: %w", err)
	}
	defer rows.Close()
	out := []LogScanRule{}
	for rows.Next() {
		r, err := scanLogScanRuleRows(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

func scanLogScanRuleRows(rows *sql.Rows) (LogScanRule, error) {
	var r LogScanRule
	var enabled int
	if err := rows.Scan(
		&r.ID, &r.WorkloadID, &r.OverridesID, &r.Name, &r.Pattern, &r.Severity, &r.Streams,
		&r.CooldownSeconds, &enabled, &r.CreatedAt, &r.UpdatedAt,
	); err != nil {
		return LogScanRule{}, fmt.Errorf("scan log scan rule: %w", err)
	}
	r.Enabled = enabled != 0
	return r, nil
}

func scanLogScanRuleRow(row *sql.Row) (LogScanRule, error) {
	var r LogScanRule
	var enabled int
	if err := row.Scan(
		&r.ID, &r.WorkloadID, &r.OverridesID, &r.Name, &r.Pattern, &r.Severity, &r.Streams,
		&r.CooldownSeconds, &enabled, &r.CreatedAt, &r.UpdatedAt,
	); err != nil {
		return LogScanRule{}, err
	}
	r.Enabled = enabled != 0
	return r, nil
}

// validateLogScanRule enforces the per-row invariants. Regex
// compilation is intentionally NOT done here — it's a hot-path
// concern owned by the engine snapshot, and engine compile errors
// become engine-side warnings rather than store-side rejections to
// keep the failure mode operator-debuggable.
func validateLogScanRule(r LogScanRule) error {
	if strings.TrimSpace(r.Name) == "" {
		return fmt.Errorf("log scan rule: name is required")
	}
	if strings.TrimSpace(r.Pattern) == "" {
		return fmt.Errorf("log scan rule: pattern is required")
	}
	switch r.Severity {
	case LogScanSeverityInfo, LogScanSeverityWarn, LogScanSeverityError:
	case "":
		// Default applied at the caller; allow blank.
	default:
		return fmt.Errorf("log scan rule: invalid severity %q", r.Severity)
	}
	switch r.Streams {
	case LogScanStreamAll, LogScanStreamStdout, LogScanStreamStderr:
	case "":
	default:
		return fmt.Errorf("log scan rule: invalid streams %q", r.Streams)
	}
	if r.CooldownSeconds < 0 {
		return fmt.Errorf("log scan rule: cooldown_seconds must be >= 0")
	}
	// An override row must reference an existing global id and live
	// under a specific workload. The store doesn't verify the FK
	// (no PRAGMA foreign_keys), but we can sanity-check the shape.
	if r.OverridesID != 0 && r.WorkloadID == "" {
		return fmt.Errorf("log scan rule: override row requires workload_id")
	}
	return nil
}
