package store

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

	"github.com/google/uuid"
)

const triggerColumns = `id, kind, name, config,
	webhook_secret, webhook_signing_secret, webhook_require_signature,
	last_fired_at, created_at, updated_at`

func scanTrigger(s rowScanner) (Trigger, error) {
	var t Trigger
	var requireSig int
	if err := s.Scan(&t.ID, &t.Kind, &t.Name, &t.Config,
		&t.WebhookSecret, &t.WebhookSigningSecret, &requireSig,
		&t.LastFiredAt, &t.CreatedAt, &t.UpdatedAt); err != nil {
		return Trigger{}, err
	}
	t.WebhookRequireSignature = requireSig != 0
	return t, nil
}

// CreateTrigger inserts a new trigger row. Kind + Name are required.
// Config is normalized to "{}" when empty; webhook secret is left empty
// unless the caller pre-populates it.
func (s *Store) CreateTrigger(t Trigger) (Trigger, error) {
	if t.ID == "" {
		t.ID = uuid.New().String()
	}
	if t.Config == "" {
		t.Config = "{}"
	}
	t.CreatedAt = Now()
	t.UpdatedAt = t.CreatedAt
	_, err := s.db.Exec(
		`INSERT INTO triggers (`+triggerColumns+`)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		t.ID, t.Kind, t.Name, t.Config,
		t.WebhookSecret, t.WebhookSigningSecret, BoolToInt(t.WebhookRequireSignature),
		t.LastFiredAt, t.CreatedAt, t.UpdatedAt,
	)
	if err != nil {
		return Trigger{}, fmt.Errorf("insert trigger: %w", translateSQLError(err))
	}
	return t, nil
}

// GetTriggerByID returns one trigger.
func (s *Store) GetTriggerByID(id string) (Trigger, error) {
	t, err := scanTrigger(s.db.QueryRow(
		`SELECT `+triggerColumns+` FROM triggers WHERE id = ?`, id,
	))
	if errors.Is(err, sql.ErrNoRows) {
		return Trigger{}, fmt.Errorf("trigger %s: %w", id, ErrNotFound)
	}
	if err != nil {
		return Trigger{}, fmt.Errorf("query trigger: %w", err)
	}
	return t, nil
}

// GetTriggerByWebhookSecret resolves a trigger by its inbound webhook
// secret. Empty input is treated as not-found to avoid accidental matches
// against rows whose webhook is disabled.
func (s *Store) GetTriggerByWebhookSecret(secret string) (Trigger, error) {
	if secret == "" {
		return Trigger{}, fmt.Errorf("empty secret: %w", ErrNotFound)
	}
	t, err := scanTrigger(s.db.QueryRow(
		`SELECT `+triggerColumns+` FROM triggers WHERE webhook_secret = ?`, secret,
	))
	if errors.Is(err, sql.ErrNoRows) {
		return Trigger{}, ErrNotFound
	}
	if err != nil {
		return Trigger{}, fmt.Errorf("query trigger by webhook secret: %w", err)
	}
	return t, nil
}

// GetTriggerByName resolves a trigger by its unique human name.
func (s *Store) GetTriggerByName(name string) (Trigger, error) {
	if name == "" {
		return Trigger{}, fmt.Errorf("empty name: %w", ErrNotFound)
	}
	t, err := scanTrigger(s.db.QueryRow(
		`SELECT `+triggerColumns+` FROM triggers WHERE name = ?`, name,
	))
	if errors.Is(err, sql.ErrNoRows) {
		return Trigger{}, ErrNotFound
	}
	if err != nil {
		return Trigger{}, fmt.Errorf("query trigger by name: %w", err)
	}
	return t, nil
}

// ListTriggers returns all triggers, ordered by name. Optional kind
// filter — empty string returns everything.
func (s *Store) ListTriggers(kind string) ([]Trigger, error) {
	var rows *sql.Rows
	var err error
	if kind == "" {
		rows, err = s.db.Query(`SELECT ` + triggerColumns + ` FROM triggers ORDER BY name`)
	} else {
		rows, err = s.db.Query(`SELECT `+triggerColumns+` FROM triggers WHERE kind = ? ORDER BY name`, kind)
	}
	if err != nil {
		return nil, fmt.Errorf("query triggers: %w", err)
	}
	defer rows.Close()

	out := []Trigger{}
	for rows.Next() {
		t, err := scanTrigger(rows)
		if err != nil {
			return nil, fmt.Errorf("scan trigger: %w", err)
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

// TriggerWithBindingCount projects a Trigger plus its current binding
// count in a single round-trip. Used by /api/triggers list rendering so
// the response avoids one COUNT(*) per trigger row.
type TriggerWithBindingCount struct {
	Trigger
	BindingCount int
}

// ListTriggersWithBindingCount returns every trigger ordered by name
// with the count of its bindings joined in. Optional kind filter.
func (s *Store) ListTriggersWithBindingCount(kind string) ([]TriggerWithBindingCount, error) {
	const base = `
		SELECT t.id, t.kind, t.name, t.config,
			t.webhook_secret, t.webhook_signing_secret, t.webhook_require_signature,
			t.last_fired_at, t.created_at, t.updated_at,
			COALESCE(b.cnt, 0)
		FROM triggers t
		LEFT JOIN (
			SELECT trigger_id, COUNT(*) AS cnt
			FROM workload_trigger_bindings
			GROUP BY trigger_id
		) b ON b.trigger_id = t.id`
	var rows *sql.Rows
	var err error
	if kind == "" {
		rows, err = s.db.Query(base + ` ORDER BY t.name`)
	} else {
		rows, err = s.db.Query(base+` WHERE t.kind = ? ORDER BY t.name`, kind)
	}
	if err != nil {
		return nil, fmt.Errorf("query triggers with binding count: %w", err)
	}
	defer rows.Close()

	out := []TriggerWithBindingCount{}
	for rows.Next() {
		var t Trigger
		var requireSig int
		var count int
		if err := rows.Scan(&t.ID, &t.Kind, &t.Name, &t.Config,
			&t.WebhookSecret, &t.WebhookSigningSecret, &requireSig,
			&t.LastFiredAt, &t.CreatedAt, &t.UpdatedAt, &count); err != nil {
			return nil, fmt.Errorf("scan trigger+count: %w", err)
		}
		t.WebhookRequireSignature = requireSig != 0
		out = append(out, TriggerWithBindingCount{Trigger: t, BindingCount: count})
	}
	return out, rows.Err()
}

// UpdateTrigger updates the mutable fields of a trigger. Kind is
// immutable post-create — changing kinds would invalidate every
// binding's interpretation of binding_config.
func (s *Store) UpdateTrigger(t Trigger) error {
	t.UpdatedAt = Now()
	if t.Config == "" {
		t.Config = "{}"
	}
	result, err := s.db.Exec(
		`UPDATE triggers SET name=?, config=?,
			webhook_secret=?, webhook_signing_secret=?, webhook_require_signature=?,
			updated_at=?
		 WHERE id=?`,
		t.Name, t.Config,
		t.WebhookSecret, t.WebhookSigningSecret, BoolToInt(t.WebhookRequireSignature),
		t.UpdatedAt, t.ID,
	)
	if err != nil {
		return fmt.Errorf("update trigger: %w", translateSQLError(err))
	}
	n, _ := result.RowsAffected()
	if n == 0 {
		return fmt.Errorf("trigger %s: %w", t.ID, ErrNotFound)
	}
	return nil
}

// DeleteTrigger removes a trigger row. Bindings cascade away via FK.
func (s *Store) DeleteTrigger(id string) error {
	result, err := s.db.Exec(`DELETE FROM triggers WHERE id = ?`, id)
	if err != nil {
		return fmt.Errorf("delete trigger: %w", err)
	}
	n, _ := result.RowsAffected()
	if n == 0 {
		return fmt.Errorf("trigger %s: %w", id, ErrNotFound)
	}
	return nil
}

// CreateTriggerWithBindingTx atomically creates a trigger row and
// binds it to a workload. Used by the workload-side inline-create-and-
// bind endpoint so a binding-insert failure does not leave an orphan
// trigger row behind. Returns the persisted trigger and binding.
func (s *Store) CreateTriggerWithBindingTx(t Trigger, b WorkloadTriggerBinding) (Trigger, WorkloadTriggerBinding, error) {
	tx, err := s.db.Begin()
	if err != nil {
		return Trigger{}, WorkloadTriggerBinding{}, fmt.Errorf("begin: %w", err)
	}
	defer func() { _ = tx.Rollback() }()

	if t.ID == "" {
		t.ID = uuid.New().String()
	}
	if t.Config == "" {
		t.Config = "{}"
	}
	t.CreatedAt = Now()
	t.UpdatedAt = t.CreatedAt
	if _, err := tx.Exec(
		`INSERT INTO triggers (`+triggerColumns+`)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		t.ID, t.Kind, t.Name, t.Config,
		t.WebhookSecret, t.WebhookSigningSecret, BoolToInt(t.WebhookRequireSignature),
		t.LastFiredAt, t.CreatedAt, t.UpdatedAt,
	); err != nil {
		return Trigger{}, WorkloadTriggerBinding{}, fmt.Errorf("insert trigger: %w", translateSQLError(err))
	}

	if b.ID == "" {
		b.ID = uuid.New().String()
	}
	if b.BindingConfig == "" {
		b.BindingConfig = "{}"
	}
	b.TriggerID = t.ID
	b.CreatedAt = t.CreatedAt
	b.UpdatedAt = t.UpdatedAt
	if _, err := tx.Exec(
		`INSERT INTO workload_trigger_bindings (`+bindingColumns+`)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
		b.ID, b.WorkloadID, b.TriggerID, b.BindingConfig,
		BoolToInt(b.Enabled), b.SortOrder, b.CreatedAt, b.UpdatedAt,
	); err != nil {
		return Trigger{}, WorkloadTriggerBinding{}, fmt.Errorf("insert binding: %w", translateSQLError(err))
	}

	if err := tx.Commit(); err != nil {
		return Trigger{}, WorkloadTriggerBinding{}, fmt.Errorf("commit: %w", err)
	}
	return t, b, nil
}

// SetTriggerWebhookSecret rotates the inbound webhook URL secret. Pass
// empty string to disable webhook ingress for this trigger.
func (s *Store) SetTriggerWebhookSecret(id, secret string) error {
	result, err := s.db.Exec(
		`UPDATE triggers SET webhook_secret=?, updated_at=? WHERE id=?`,
		secret, Now(), id,
	)
	if err != nil {
		return fmt.Errorf("update trigger webhook_secret: %w", err)
	}
	n, _ := result.RowsAffected()
	if n == 0 {
		return fmt.Errorf("trigger %s: %w", id, ErrNotFound)
	}
	return nil
}

// EnsureTriggerWebhookSecret returns the current secret, generating one
// lazily for triggers that have none. Mirrors the workload helper.
func (s *Store) EnsureTriggerWebhookSecret(id string) (string, error) {
	t, err := s.GetTriggerByID(id)
	if err != nil {
		return "", err
	}
	if t.WebhookSecret != "" {
		return t.WebhookSecret, nil
	}
	secret := generateWebhookSecret()
	if err := s.SetTriggerWebhookSecret(id, secret); err != nil {
		return "", err
	}
	return secret, nil
}

// SetTriggerLastFired records the wall-clock the scheduler last
// dispatched this trigger. Callers pass time.Now().UTC().Format(time.RFC3339)
// so the value is stable across timezones. Updating last_fired_at does
// not bump updated_at — last_fired_at is operational state, while
// updated_at tracks user-visible config edits.
//
// ts must parse as RFC3339 — a defense-in-depth check so a careless
// caller cannot corrupt the column with a garbage string the scheduler
// would refuse to parse on every tick. To clear the column (effectively
// "fire on next tick"), use a separate API rather than passing empty
// here; the narrow contract keeps the call site grep-able and forces
// any reset-cadence flow to be explicitly designed and authorized.
func (s *Store) SetTriggerLastFired(id, ts string) error {
	if _, err := time.Parse(time.RFC3339, ts); err != nil {
		return fmt.Errorf("invalid last_fired_at %q (want RFC3339): %w", ts, err)
	}
	result, err := s.db.Exec(
		`UPDATE triggers SET last_fired_at = ? WHERE id = ?`,
		ts, id,
	)
	if err != nil {
		return fmt.Errorf("update trigger last_fired_at: %w", err)
	}
	n, _ := result.RowsAffected()
	if n == 0 {
		return fmt.Errorf("trigger %s: %w", id, ErrNotFound)
	}
	return nil
}

