package store

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

// AuditFilter narrows ListAudit. Empty fields match anything.
type AuditFilter struct {
	UserID     string    // empty matches any user OR system rows
	Actor      string    // user | agent | system | "" (any)
	Action     string    // exact match (e.g. "host.enrolled")
	ActionLike string    // substring match (e.g. "alert." matches alert.acknowledge / alert.resolve)
	TargetKind string    // host | source_group | alert | notification_channel | "" (any)
	TargetID   string    // exact match on target_id
	Since      time.Time // zero = no lower bound
	Until      time.Time // zero = no upper bound
	Limit      int       // 0 = no limit
	// OrderBy is one of "ts" | "actor" | "user_id" | "action" |
	// "target_kind". Empty / unknown falls back to "ts". The
	// allowlist is enforced inside ListAudit so callers can't
	// inject SQL via this field.
	OrderBy  string
	OrderAsc bool // false = DESC (default — newest first)
}

// auditOrderColumn validates f.OrderBy against the column allowlist
// and returns the SQL fragment. Unknown / empty → "ts" so callers
// always get a deterministic order.
func auditOrderColumn(s string) string {
	switch s {
	case "actor", "user_id", "action", "target_kind":
		return s
	default:
		return "ts"
	}
}

// ListAudit returns audit_log rows ordered by ts DESC.
func (s *Store) ListAudit(ctx context.Context, f AuditFilter) ([]AuditEntry, error) {
	q := `SELECT id, user_id, actor, action, target_kind, target_id, ts, payload FROM audit_log`
	conds := []string{}
	args := []any{}
	if f.UserID != "" {
		conds = append(conds, "user_id = ?")
		args = append(args, f.UserID)
	}
	if f.Actor != "" {
		conds = append(conds, "actor = ?")
		args = append(args, f.Actor)
	}
	if f.Action != "" {
		conds = append(conds, "action = ?")
		args = append(args, f.Action)
	}
	if f.ActionLike != "" {
		conds = append(conds, "action LIKE ?")
		args = append(args, "%"+f.ActionLike+"%")
	}
	if f.TargetKind != "" {
		conds = append(conds, "target_kind = ?")
		args = append(args, f.TargetKind)
	}
	if f.TargetID != "" {
		conds = append(conds, "target_id = ?")
		args = append(args, f.TargetID)
	}
	if !f.Since.IsZero() {
		conds = append(conds, "ts >= ?")
		args = append(args, f.Since.UTC().Format(time.RFC3339Nano))
	}
	if !f.Until.IsZero() {
		conds = append(conds, "ts <= ?")
		args = append(args, f.Until.UTC().Format(time.RFC3339Nano))
	}
	if len(conds) > 0 {
		q += " WHERE " + strings.Join(conds, " AND ")
	}
	col := auditOrderColumn(f.OrderBy)
	dir := "DESC"
	if f.OrderAsc {
		dir = "ASC"
	}
	// Always tie-break on ts DESC so equal sort keys (e.g. dozens
	// of rows with action='alert.resolve') still come back in a
	// deterministic, time-meaningful order.
	if col == "ts" {
		q += fmt.Sprintf(" ORDER BY ts %s", dir)
	} else {
		q += fmt.Sprintf(" ORDER BY %s %s, ts DESC", col, dir)
	}
	if f.Limit > 0 {
		q += ` LIMIT ?`
		args = append(args, f.Limit)
	}
	rows, err := s.db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, fmt.Errorf("store: list audit: %w", err)
	}
	defer func() { _ = rows.Close() }()
	var out []AuditEntry
	for rows.Next() {
		e, err := scanAuditRow(rows.Scan)
		if err != nil {
			return nil, err
		}
		out = append(out, *e)
	}
	return out, rows.Err()
}

// DistinctAuditActions returns the set of distinct action strings
// currently present in the table — used to populate the action filter
// dropdown so the operator picks from what actually exists, not a
// hardcoded list that might drift from the codebase.
func (s *Store) DistinctAuditActions(ctx context.Context) ([]string, error) {
	rows, err := s.db.QueryContext(ctx,
		`SELECT DISTINCT action FROM audit_log ORDER BY action`)
	if err != nil {
		return nil, fmt.Errorf("store: distinct audit actions: %w", err)
	}
	defer func() { _ = rows.Close() }()
	var out []string
	for rows.Next() {
		var a string
		if err := rows.Scan(&a); err != nil {
			return nil, err
		}
		out = append(out, a)
	}
	return out, rows.Err()
}

func scanAuditRow(scan func(...any) error) (*AuditEntry, error) {
	var e AuditEntry
	var userID, targetKind, targetID, payload sql.NullString
	var ts string
	if err := scan(&e.ID, &userID, &e.Actor, &e.Action, &targetKind, &targetID, &ts, &payload); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, fmt.Errorf("store: scan audit: %w", err)
	}
	if userID.Valid {
		v := userID.String
		e.UserID = &v
	}
	if targetKind.Valid {
		v := targetKind.String
		e.TargetKind = &v
	}
	if targetID.Valid {
		v := targetID.String
		e.TargetID = &v
	}
	t, err := time.Parse(time.RFC3339Nano, ts)
	if err != nil {
		return nil, fmt.Errorf("store: parse audit ts: %w", err)
	}
	e.TS = t
	if payload.Valid && payload.String != "" {
		e.Payload = json.RawMessage(payload.String)
	}
	return &e, nil
}

// AppendAudit records an audit log entry.
func (s *Store) AppendAudit(ctx context.Context, e AuditEntry) error {
	if len(e.Payload) == 0 {
		e.Payload = json.RawMessage("{}")
	}
	_, err := s.db.ExecContext(ctx,
		`INSERT INTO audit_log (id, user_id, actor, action, target_kind, target_id, ts, payload)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
		e.ID, nullable(e.UserID), e.Actor, e.Action,
		nullable(e.TargetKind), nullable(e.TargetID),
		e.TS.UTC().Format(time.RFC3339Nano),
		string(e.Payload))
	if err != nil {
		return fmt.Errorf("store: append audit: %w", err)
	}
	return nil
}

// nullable returns nil for nil/empty *string so SQLite stores NULL.
// SQLite's driver treats Go nil as NULL but treats *string("") as ”.
// We want NULL semantics for "absent."
func nullable(p *string) any {
	if p == nil || *p == "" {
		return nil
	}
	return *p
}
