package store

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

// Snapshot mirrors the snapshots projection table.
type Snapshot struct {
	ID          string
	HostID      string
	ShortID     string
	Time        time.Time
	Hostname    string
	Paths       []string
	Tags        []string
	SizeBytes   int64
	FileCount   int64
	RefreshedAt time.Time
}

// ReplaceHostSnapshots atomically replaces the snapshot projection for
// one host. Snapshots are reported by the agent in full after each
// successful backup, so we treat the message as the new source of
// truth and delete-then-insert under one transaction.
//
// snapshot_count on the host row is updated in the same tx so the
// dashboard's per-host count is always consistent with the snapshot
// list the host detail page renders.
func (s *Store) ReplaceHostSnapshots(ctx context.Context, hostID string, snaps []Snapshot, when time.Time) error {
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("store: begin snapshots tx: %w", err)
	}
	defer func() { _ = tx.Rollback() }()

	if _, err := tx.ExecContext(ctx,
		`DELETE FROM snapshots WHERE host_id = ?`, hostID); err != nil {
		return fmt.Errorf("store: clear snapshots for host: %w", err)
	}

	if len(snaps) > 0 {
		stmt, err := tx.PrepareContext(ctx,
			`INSERT INTO snapshots (
				id, host_id, short_id, time, hostname, paths, tags,
				size_bytes, file_count, refreshed_at
			 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
		if err != nil {
			return fmt.Errorf("store: prepare snapshot insert: %w", err)
		}
		defer func() { _ = stmt.Close() }()

		refreshed := when.UTC().Format(time.RFC3339Nano)
		for _, snap := range snaps {
			paths, _ := json.Marshal(snap.Paths)
			tags, _ := json.Marshal(snap.Tags)
			if _, err := stmt.ExecContext(ctx,
				snap.ID, hostID, snap.ShortID,
				snap.Time.UTC().Format(time.RFC3339Nano),
				snap.Hostname, string(paths), string(tags),
				snap.SizeBytes, snap.FileCount, refreshed,
			); err != nil {
				return fmt.Errorf("store: insert snapshot %s: %w", snap.ID, err)
			}
		}
	}

	if _, err := tx.ExecContext(ctx,
		`UPDATE hosts SET snapshot_count = ? WHERE id = ?`,
		len(snaps), hostID); err != nil {
		return fmt.Errorf("store: update host snapshot_count: %w", err)
	}

	if err := tx.Commit(); err != nil {
		return fmt.Errorf("store: commit snapshots: %w", err)
	}
	return nil
}

// ListSnapshotsByHost returns the cached snapshot list for a host,
// most-recent first. Empty slice is a normal "no snapshots yet" case.
func (s *Store) ListSnapshotsByHost(ctx context.Context, hostID string) ([]Snapshot, error) {
	rows, err := s.db.QueryContext(ctx,
		`SELECT id, host_id, short_id, time, hostname, paths, tags,
			size_bytes, file_count, refreshed_at
		 FROM snapshots
		 WHERE host_id = ?
		 ORDER BY time DESC`, hostID)
	if err != nil {
		return nil, fmt.Errorf("store: list snapshots: %w", err)
	}
	defer func() { _ = rows.Close() }()

	var out []Snapshot
	for rows.Next() {
		snap, err := scanSnapshotRow(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, *snap)
	}
	return out, rows.Err()
}

func scanSnapshotRow(r *sql.Rows) (*Snapshot, error) {
	var (
		snap        Snapshot
		t, refresh  string
		paths, tags string
	)
	if err := r.Scan(&snap.ID, &snap.HostID, &snap.ShortID,
		&t, &snap.Hostname, &paths, &tags,
		&snap.SizeBytes, &snap.FileCount, &refresh); err != nil {
		return nil, fmt.Errorf("store: scan snapshot: %w", err)
	}
	snap.Time, _ = time.Parse(time.RFC3339Nano, t)
	snap.RefreshedAt, _ = time.Parse(time.RFC3339Nano, refresh)
	if paths != "" {
		_ = json.Unmarshal([]byte(paths), &snap.Paths)
	}
	if tags != "" {
		_ = json.Unmarshal([]byte(tags), &snap.Tags)
	}
	return &snap, nil
}
