package rules

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

	"gopkg.in/yaml.v3"
)

// LoaderFileName is the operator-facing rules file.
const LoaderFileName = "notification-rules.yaml"

// LoadDocument parses + validates the YAML.
func LoadDocument(data []byte) (*Document, error) {
	// Custom unmarshal: yaml.v3 doesn't support `,inline` on a string field
	// alongside a map, so we hand-parse the trigger node into TriggerConfig.
	var raw struct {
		Rules []rawRule `yaml:"rules"`
	}
	if err := yaml.Unmarshal(data, &raw); err != nil {
		return nil, fmt.Errorf("rules: parse yaml: %w", err)
	}
	doc := &Document{Rules: make([]Rule, 0, len(raw.Rules))}
	for i, rr := range raw.Rules {
		if rr.Name == "" {
			return nil, fmt.Errorf("rules[%d]: name required", i)
		}
		if rr.Trigger.Type == "" {
			return nil, fmt.Errorf("rules[%d] (%s): trigger.type required", i, rr.Name)
		}
		if len(rr.Channels) == 0 {
			rr.Channels = []string{"dashboard"}
		}
		if rr.CooldownSeconds < 0 {
			return nil, fmt.Errorf("rules[%d] (%s): cooldown_seconds must be non-negative", i, rr.Name)
		}
		cfg := map[string]any{}
		for k, v := range rr.Trigger.Rest {
			cfg[k] = v
		}
		doc.Rules = append(doc.Rules, Rule{
			Name:            rr.Name,
			Enabled:         rr.Enabled,
			Trigger:         TriggerConfig{Type: rr.Trigger.Type, Config: cfg},
			Channels:        rr.Channels,
			CooldownSeconds: rr.CooldownSeconds,
			ChannelConfig:   rr.ChannelConfig,
		})
	}
	return doc, nil
}

// rawRule mirrors the on-disk shape with a flexible trigger field. The
// trigger node carries `type` plus arbitrary keys consumed by the trigger
// implementation.
type rawRule struct {
	Name            string                    `yaml:"name"`
	Enabled         *bool                     `yaml:"enabled,omitempty"`
	Trigger         rawTrigger                `yaml:"trigger"`
	Channels        []string                  `yaml:"channels"`
	CooldownSeconds int                       `yaml:"cooldown_seconds"`
	ChannelConfig   map[string]map[string]any `yaml:"channel_config,omitempty"`
}

type rawTrigger struct {
	Type string                 `yaml:"type"`
	Rest map[string]any         `yaml:",inline"`
}

// LoadFromDir reads notification-rules.yaml from dir; absent file is OK
// (operator may start with no rules).
func LoadFromDir(dir string) (*Document, error) {
	path := filepath.Join(dir, LoaderFileName)
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return &Document{}, nil
		}
		return nil, fmt.Errorf("rules: read %s: %w", path, err)
	}
	return LoadDocument(data)
}

// SyncToDatabase upserts the YAML-sourced rules into the notification_rules
// table. Rules whose name no longer appears in YAML and whose source is
// 'yaml' are removed; rules added later via the API would have source='api'
// and are not touched.
func SyncToDatabase(ctx context.Context, db *sql.DB, doc *Document) error {
	if db == nil {
		return nil
	}
	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()

	// Build the wanted-name set.
	wanted := map[string]struct{}{}
	now := time.Now().Unix()
	for _, r := range doc.Rules {
		wanted[r.Name] = struct{}{}
		channelsJSON, _ := json.Marshal(r.Channels)
		cfg := map[string]any{"trigger": map[string]any{
			"type":   r.Trigger.Type,
			"config": r.Trigger.Config,
		}}
		if r.ChannelConfig != nil {
			cfg["channel_config"] = r.ChannelConfig
		}
		triggerJSON, _ := json.Marshal(cfg)
		cooldown := r.CooldownSeconds
		if cooldown <= 0 {
			cooldown = 3600
		}
		enabled := 0
		if r.IsEnabled() {
			enabled = 1
		}
		_, err := tx.ExecContext(ctx, `
			INSERT INTO notification_rules
			  (name, enabled, trigger_type, trigger_config_json, channels_json,
			   cooldown_seconds, source, created_at, updated_at)
			VALUES (?, ?, ?, ?, ?, ?, 'yaml', ?, ?)
			ON CONFLICT(name) DO UPDATE SET
			  enabled = excluded.enabled,
			  trigger_type = excluded.trigger_type,
			  trigger_config_json = excluded.trigger_config_json,
			  channels_json = excluded.channels_json,
			  cooldown_seconds = excluded.cooldown_seconds,
			  updated_at = excluded.updated_at
		`, r.Name, enabled, r.Trigger.Type, string(triggerJSON), string(channelsJSON),
			cooldown, now, now)
		if err != nil {
			// notification_rules.name has no UNIQUE constraint in Phase 6's
			// migration — the operator's name is the human key here. Fall
			// back to insert-or-update by name explicitly.
			if alt := upsertByName(ctx, tx, r, channelsJSON, triggerJSON, cooldown, enabled, now); alt != nil {
				return alt
			}
		}
	}

	// Remove YAML-sourced rules no longer in the document.
	rows, err := tx.QueryContext(ctx, `SELECT id, name FROM notification_rules WHERE source = 'yaml'`)
	if err != nil {
		return err
	}
	var stale []int64
	for rows.Next() {
		var id int64
		var name string
		if err := rows.Scan(&id, &name); err != nil {
			_ = rows.Close()
			return err
		}
		if _, keep := wanted[name]; !keep {
			stale = append(stale, id)
		}
	}
	_ = rows.Close()
	for _, id := range stale {
		_, _ = tx.ExecContext(ctx, `DELETE FROM notification_rules WHERE id = ?`, id)
	}
	return tx.Commit()
}

// upsertByName is the fallback path when the UNIQUE(name) conflict clause
// isn't usable (migration without that constraint, older SQLite). It
// performs an explicit UPDATE-or-INSERT.
func upsertByName(ctx context.Context, tx *sql.Tx, r Rule, channelsJSON, triggerJSON []byte, cooldown, enabled int, now int64) error {
	res, err := tx.ExecContext(ctx, `
		UPDATE notification_rules
		SET enabled = ?, trigger_type = ?, trigger_config_json = ?,
		    channels_json = ?, cooldown_seconds = ?, updated_at = ?
		WHERE name = ? AND source = 'yaml'
	`, enabled, r.Trigger.Type, string(triggerJSON), string(channelsJSON), cooldown, now, r.Name)
	if err != nil {
		return err
	}
	n, _ := res.RowsAffected()
	if n > 0 {
		return nil
	}
	_, err = tx.ExecContext(ctx, `
		INSERT INTO notification_rules
		  (name, enabled, trigger_type, trigger_config_json, channels_json,
		   cooldown_seconds, source, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, 'yaml', ?, ?)
	`, r.Name, enabled, r.Trigger.Type, string(triggerJSON), string(channelsJSON),
		cooldown, now, now)
	return err
}
