package store

import (
	"context"
	"database/sql"
	"fmt"
	"log/slog"
	"strings"
	"time"
)

const DefaultAllocHistoryTTL = 7 * 24 * time.Hour

var DefaultEvictorInterval = 1 * time.Hour

type AllocHistoryEntry struct {
	AllocID   string    `json:"alloc_id"`
	JobID     string    `json:"job_id"`
	NodeID    string    `json:"node_id"`
	Namespace string    `json:"namespace"`
	FromState string    `json:"from_state,omitempty"`
	ToState   string    `json:"to_state"`
	Timestamp time.Time `json:"timestamp"`
	Reason    string    `json:"reason,omitempty"`
}

type HistoryFilter struct {
	JobID     string
	NodeID    string
	Namespace string
	Since     time.Time
}

type AllocHistoryRepo struct {
	db *sql.DB
}

func NewAllocHistoryRepo(db *sql.DB) *AllocHistoryRepo {
	return &AllocHistoryRepo{db: db}
}

const allocHistorySchema = `
CREATE TABLE IF NOT EXISTS alloc_history (
	alloc_id   TEXT NOT NULL,
	job_id     TEXT NOT NULL,
	node_id    TEXT NOT NULL,
	namespace  TEXT NOT NULL,
	from_state TEXT,
	to_state   TEXT NOT NULL,
	timestamp  INTEGER NOT NULL,
	reason     TEXT
);
CREATE INDEX IF NOT EXISTS idx_alloc_history_ts  ON alloc_history(timestamp);
CREATE INDEX IF NOT EXISTS idx_alloc_history_job ON alloc_history(job_id);
`

func (r *AllocHistoryRepo) EnsureSchema(ctx context.Context) error {
	if _, err := r.db.ExecContext(ctx, allocHistorySchema); err != nil {
		return fmt.Errorf("alloc history schema: %w", err)
	}
	return nil
}

func (r *AllocHistoryRepo) Record(ctx context.Context, entry AllocHistoryEntry) error {
	if err := r.EnsureSchema(ctx); err != nil {
		return err
	}
	if entry.Timestamp.IsZero() {
		entry.Timestamp = time.Now().UTC()
	}
	ts := entry.Timestamp.UnixNano()
	_, err := r.db.ExecContext(ctx,
		`INSERT INTO alloc_history (alloc_id, job_id, node_id, namespace, from_state, to_state, timestamp, reason)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
		entry.AllocID, entry.JobID, entry.NodeID, entry.Namespace,
		nullableStr(entry.FromState), entry.ToState, ts, nullableStr(entry.Reason))
	if err != nil {
		return fmt.Errorf("alloc history record: %w", err)
	}
	return nil
}

func (r *AllocHistoryRepo) List(ctx context.Context, filter HistoryFilter) ([]AllocHistoryEntry, error) {
	if err := r.EnsureSchema(ctx); err != nil {
		return nil, err
	}
	q := `SELECT alloc_id, job_id, node_id, namespace,
		COALESCE(from_state, ''), to_state, timestamp, COALESCE(reason, '')
		FROM alloc_history`
	var (
		conds []string
		args  []any
	)
	if filter.JobID != "" {
		conds = append(conds, "job_id = ?")
		args = append(args, filter.JobID)
	}
	if filter.NodeID != "" {
		conds = append(conds, "node_id = ?")
		args = append(args, filter.NodeID)
	}
	if filter.Namespace != "" {
		conds = append(conds, "namespace = ?")
		args = append(args, filter.Namespace)
	}
	if !filter.Since.IsZero() {
		conds = append(conds, "timestamp >= ?")
		args = append(args, filter.Since.UnixNano())
	}
	if len(conds) > 0 {
		q += " WHERE " + strings.Join(conds, " AND ")
	}
	q += " ORDER BY timestamp ASC"

	rows, err := r.db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, fmt.Errorf("alloc history list: %w", err)
	}
	defer rows.Close()
	var out []AllocHistoryEntry
	for rows.Next() {
		var (
			e    AllocHistoryEntry
			ts   int64
			from string
			reas string
		)
		if err := rows.Scan(&e.AllocID, &e.JobID, &e.NodeID, &e.Namespace,
			&from, &e.ToState, &ts, &reas); err != nil {
			return nil, fmt.Errorf("alloc history scan: %w", err)
		}
		e.FromState = from
		e.Reason = reas
		e.Timestamp = time.Unix(0, ts).UTC()
		out = append(out, e)
	}
	return out, rows.Err()
}

func (r *AllocHistoryRepo) Evict(ctx context.Context, before time.Time) (int64, error) {
	if err := r.EnsureSchema(ctx); err != nil {
		return 0, err
	}
	res, err := r.db.ExecContext(ctx,
		`DELETE FROM alloc_history WHERE timestamp < ?`, before.UnixNano())
	if err != nil {
		return 0, fmt.Errorf("alloc history evict: %w", err)
	}
	n, _ := res.RowsAffected()
	return n, nil
}

func (r *AllocHistoryRepo) StartEvictor(ctx context.Context, ttl, interval time.Duration) (stop func()) {
	if ttl <= 0 {
		ttl = DefaultAllocHistoryTTL
	}
	if interval <= 0 {
		interval = DefaultEvictorInterval
	}
	ctx, cancel := context.WithCancel(ctx)
	done := make(chan struct{})
	go func() {
		defer close(done)
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		sweep := func() {
			cutoff := time.Now().UTC().Add(-ttl)
			n, err := r.Evict(ctx, cutoff)
			if err != nil {
				slog.Default().Warn("alloc history evictor: sweep failed", "error", err)
				return
			}
			if n > 0 {
				slog.Default().Info("alloc history evictor: evicted", "count", n, "older_than", cutoff.Format(time.RFC3339))
			}
		}
		sweep()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				sweep()
			}
		}
	}()
	return func() {
		cancel()
		<-done
	}
}

func nullableStr(s string) any {
	if s == "" {
		return nil
	}
	return s
}
