package store

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"path/filepath"
	"sync"
	"time"

	_ "modernc.org/sqlite"

	"banger/internal/model"
)

type Store struct {
	db      *sql.DB
	writeMu sync.Mutex
}

func Open(path string) (*Store, error) {
	dsn, err := sqliteDSN(path)
	if err != nil {
		return nil, err
	}
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, err
	}
	store := &Store{db: db}
	if err := runMigrations(db); err != nil {
		_ = db.Close()
		return nil, err
	}
	return store, nil
}

// OpenReadOnly opens the state DB without running migrations and with
// SQLite's mode=ro flag so no write can slip through — the file and
// its WAL sidecar stay untouched. Used by `banger doctor`, which must
// be pure inspection: running it should never mutate user state, and
// it must not trigger a schema migration the user didn't ask for.
//
// Returns the usual sql.ErrNoRows-compatible errors from the read
// queries if the DB's schema is older than the current code expects;
// doctor surfaces those as failing checks rather than a hard crash.
func OpenReadOnly(path string) (*Store, error) {
	dsn, err := sqliteReadOnlyDSN(path)
	if err != nil {
		return nil, err
	}
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, err
	}
	// Ping forces SQLite to actually open the file, so a missing or
	// unreadable DB fails here rather than at first query. Match the
	// existing Open contract: caller expects success to mean "ready
	// to read."
	if err := db.Ping(); err != nil {
		_ = db.Close()
		return nil, err
	}
	return &Store{db: db}, nil
}

func (s *Store) Close() error {
	return s.db.Close()
}

func sqliteDSN(path string) (string, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		return "", fmt.Errorf("resolve sqlite path: %w", err)
	}
	query := url.Values{}
	for _, pragma := range []string{
		"journal_mode(WAL)",
		"synchronous(NORMAL)",
		"foreign_keys(1)",
		"busy_timeout(5000)",
		"temp_store(MEMORY)",
		"wal_autocheckpoint(1000)",
		"journal_size_limit(67108864)",
	} {
		query.Add("_pragma", pragma)
	}
	return (&url.URL{
		Scheme:   "file",
		Path:     filepath.ToSlash(absPath),
		RawQuery: query.Encode(),
	}).String(), nil
}

// sqliteReadOnlyDSN builds a DSN that opens the DB in SQLite's
// read-only mode. Deliberately omits journal_mode=WAL and the other
// write-adjacent pragmas set by sqliteDSN — mode=ro refuses them
// anyway, and keeping the list minimal means the query never touches
// the file. foreign_keys and busy_timeout are the only pragmas worth
// keeping for read paths (semantics parity + lock backoff).
func sqliteReadOnlyDSN(path string) (string, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		return "", fmt.Errorf("resolve sqlite path: %w", err)
	}
	query := url.Values{}
	query.Set("mode", "ro")
	query.Add("_pragma", "foreign_keys(1)")
	query.Add("_pragma", "busy_timeout(5000)")
	return (&url.URL{
		Scheme:   "file",
		Path:     filepath.ToSlash(absPath),
		RawQuery: query.Encode(),
	}).String(), nil
}

func (s *Store) UpsertImage(ctx context.Context, image model.Image) error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	const query = `
	INSERT INTO images (
		id, name, managed, artifact_dir, rootfs_path, work_seed_path, kernel_path, initrd_path,
		modules_dir, build_size, seeded_ssh_public_key_fingerprint, created_at, updated_at
	) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
	ON CONFLICT(id) DO UPDATE SET
		name=excluded.name,
		managed=excluded.managed,
		artifact_dir=excluded.artifact_dir,
		rootfs_path=excluded.rootfs_path,
		work_seed_path=excluded.work_seed_path,
		kernel_path=excluded.kernel_path,
		initrd_path=excluded.initrd_path,
		modules_dir=excluded.modules_dir,
		build_size=excluded.build_size,
		seeded_ssh_public_key_fingerprint=excluded.seeded_ssh_public_key_fingerprint,
		updated_at=excluded.updated_at`
	_, err := s.db.ExecContext(ctx, query,
		image.ID,
		image.Name,
		boolToInt(image.Managed),
		image.ArtifactDir,
		image.RootfsPath,
		image.WorkSeedPath,
		image.KernelPath,
		image.InitrdPath,
		image.ModulesDir,
		image.BuildSize,
		image.SeededSSHPublicKeyFingerprint,
		image.CreatedAt.Format(time.RFC3339),
		image.UpdatedAt.Format(time.RFC3339),
	)
	return err
}

func (s *Store) GetImageByName(ctx context.Context, name string) (model.Image, error) {
	return s.getImage(ctx, "SELECT id, name, managed, artifact_dir, rootfs_path, work_seed_path, kernel_path, initrd_path, modules_dir, build_size, seeded_ssh_public_key_fingerprint, created_at, updated_at FROM images WHERE name = ?", name)
}

func (s *Store) GetImageByID(ctx context.Context, id string) (model.Image, error) {
	return s.getImage(ctx, "SELECT id, name, managed, artifact_dir, rootfs_path, work_seed_path, kernel_path, initrd_path, modules_dir, build_size, seeded_ssh_public_key_fingerprint, created_at, updated_at FROM images WHERE id = ?", id)
}

func (s *Store) ListImages(ctx context.Context) ([]model.Image, error) {
	rows, err := s.db.QueryContext(ctx, "SELECT id, name, managed, artifact_dir, rootfs_path, work_seed_path, kernel_path, initrd_path, modules_dir, build_size, seeded_ssh_public_key_fingerprint, created_at, updated_at FROM images ORDER BY created_at ASC")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var images []model.Image
	for rows.Next() {
		image, err := scanImage(rows)
		if err != nil {
			return nil, err
		}
		images = append(images, image)
	}
	return images, rows.Err()
}

func (s *Store) DeleteImage(ctx context.Context, id string) error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	_, err := s.db.ExecContext(ctx, "DELETE FROM images WHERE id = ?", id)
	return err
}

func (s *Store) UpsertVM(ctx context.Context, vm model.VMRecord) error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	specJSON, err := json.Marshal(vm.Spec)
	if err != nil {
		return err
	}
	runtimeJSON, err := json.Marshal(vm.Runtime)
	if err != nil {
		return err
	}
	statsJSON, err := json.Marshal(vm.Stats)
	if err != nil {
		return err
	}
	const query = `
	INSERT INTO vms (
		id, name, image_id, guest_ip, state, created_at, updated_at, last_touched_at,
		spec_json, runtime_json, stats_json
	) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
	ON CONFLICT(id) DO UPDATE SET
		name=excluded.name,
		image_id=excluded.image_id,
		guest_ip=excluded.guest_ip,
		state=excluded.state,
		updated_at=excluded.updated_at,
		last_touched_at=excluded.last_touched_at,
		spec_json=excluded.spec_json,
		runtime_json=excluded.runtime_json,
		stats_json=excluded.stats_json`
	_, err = s.db.ExecContext(ctx, query,
		vm.ID,
		vm.Name,
		vm.ImageID,
		vm.Runtime.GuestIP,
		string(vm.State),
		vm.CreatedAt.Format(time.RFC3339),
		vm.UpdatedAt.Format(time.RFC3339),
		vm.LastTouchedAt.Format(time.RFC3339),
		string(specJSON),
		string(runtimeJSON),
		string(statsJSON),
	)
	return err
}

func (s *Store) GetVM(ctx context.Context, idOrName string) (model.VMRecord, error) {
	const query = `
	SELECT id, name, image_id, guest_ip, state, created_at, updated_at, last_touched_at,
	       spec_json, runtime_json, stats_json, workspace_json
	FROM vms
	WHERE id = ? OR name = ?
	`
	row := s.db.QueryRowContext(ctx, query, idOrName, idOrName)
	return scanVMRow(row)
}

func (s *Store) GetVMByID(ctx context.Context, id string) (model.VMRecord, error) {
	row := s.db.QueryRowContext(ctx, `
	SELECT id, name, image_id, guest_ip, state, created_at, updated_at, last_touched_at,
	       spec_json, runtime_json, stats_json, workspace_json
	FROM vms WHERE id = ?`, id)
	return scanVMRow(row)
}

// GetVMByName is the exact-name lookup used for creation-time
// uniqueness checks. Unlike GetVM (which matches id OR name) and
// Daemon.FindVM (which also falls back to prefix-matching), this
// returns sql.ErrNoRows for anything except a literal name hit, so
// a new VM can't be rejected just because its name prefixes an
// existing VM's id or an existing VM's name.
func (s *Store) GetVMByName(ctx context.Context, name string) (model.VMRecord, error) {
	row := s.db.QueryRowContext(ctx, `
	SELECT id, name, image_id, guest_ip, state, created_at, updated_at, last_touched_at,
	       spec_json, runtime_json, stats_json, workspace_json
	FROM vms WHERE name = ?`, name)
	return scanVMRow(row)
}

func (s *Store) ListVMs(ctx context.Context) ([]model.VMRecord, error) {
	rows, err := s.db.QueryContext(ctx, `
	SELECT id, name, image_id, guest_ip, state, created_at, updated_at, last_touched_at,
	       spec_json, runtime_json, stats_json, workspace_json
	FROM vms ORDER BY created_at ASC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var vms []model.VMRecord
	for rows.Next() {
		vm, err := scanVMRows(rows)
		if err != nil {
			return nil, err
		}
		vms = append(vms, vm)
	}
	return vms, rows.Err()
}

func (s *Store) DeleteVM(ctx context.Context, id string) error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	_, err := s.db.ExecContext(ctx, "DELETE FROM vms WHERE id = ?", id)
	return err
}

// SetVMWorkspace persists the workspace state from a workspace.prepare
// result onto the VM row. Called after a successful prepare so the
// guest path, host source path, and HEAD commit survive daemon
// restarts and are available to `vm exec` without re-stating them.
// Best-effort from the caller's perspective — a failure here does not
// roll back the prepare itself.
func (s *Store) SetVMWorkspace(ctx context.Context, vmID string, workspace model.VMWorkspace) error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	data, err := json.Marshal(workspace)
	if err != nil {
		return err
	}
	_, err = s.db.ExecContext(ctx, "UPDATE vms SET workspace_json = ? WHERE id = ?", string(data), vmID)
	return err
}

func (s *Store) FindVMsUsingImage(ctx context.Context, imageID string) ([]model.VMRecord, error) {
	rows, err := s.db.QueryContext(ctx, `
	SELECT id, name, image_id, guest_ip, state, created_at, updated_at, last_touched_at,
	       spec_json, runtime_json, stats_json, workspace_json
	FROM vms WHERE image_id = ?`, imageID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var vms []model.VMRecord
	for rows.Next() {
		vm, err := scanVMRows(rows)
		if err != nil {
			return nil, err
		}
		vms = append(vms, vm)
	}
	return vms, rows.Err()
}

func (s *Store) NextGuestIP(ctx context.Context, bridgeIPPrefix string) (string, error) {
	used := map[string]struct{}{}
	rows, err := s.db.QueryContext(ctx, "SELECT guest_ip FROM vms")
	if err != nil {
		return "", err
	}
	defer rows.Close()
	for rows.Next() {
		var ip string
		if err := rows.Scan(&ip); err != nil {
			return "", err
		}
		used[ip] = struct{}{}
	}
	if err := rows.Err(); err != nil {
		return "", err
	}
	for i := 2; i < 255; i++ {
		candidate := fmt.Sprintf("%s.%d", bridgeIPPrefix, i)
		if _, exists := used[candidate]; !exists {
			return candidate, nil
		}
	}
	return "", errors.New("no guest IPs available")
}

func (s *Store) getImage(ctx context.Context, query string, arg string) (model.Image, error) {
	row := s.db.QueryRowContext(ctx, query, arg)
	return scanImageRow(row)
}

func scanImage(rows scanner) (model.Image, error) {
	return scanImageRow(rows)
}

type scanner interface {
	Scan(dest ...any) error
}

func scanImageRow(row scanner) (model.Image, error) {
	var image model.Image
	var managed int
	var workSeedPath sql.NullString
	var seededSSHPublicKeyFingerprint sql.NullString
	var createdAt, updatedAt string
	err := row.Scan(
		&image.ID,
		&image.Name,
		&managed,
		&image.ArtifactDir,
		&image.RootfsPath,
		&workSeedPath,
		&image.KernelPath,
		&image.InitrdPath,
		&image.ModulesDir,
		&image.BuildSize,
		&seededSSHPublicKeyFingerprint,
		&createdAt,
		&updatedAt,
	)
	if err != nil {
		return image, err
	}
	image.Managed = managed == 1
	image.WorkSeedPath = workSeedPath.String
	image.SeededSSHPublicKeyFingerprint = seededSSHPublicKeyFingerprint.String
	image.CreatedAt, err = time.Parse(time.RFC3339, createdAt)
	if err != nil {
		return image, err
	}
	image.UpdatedAt, err = time.Parse(time.RFC3339, updatedAt)
	if err != nil {
		return image, err
	}
	return image, nil
}

func scanVMRow(row scanner) (model.VMRecord, error) {
	return scanVMInto(row)
}

func scanVMRows(rows scanner) (model.VMRecord, error) {
	return scanVMInto(rows)
}

func scanVMInto(row scanner) (model.VMRecord, error) {
	var vm model.VMRecord
	var state, createdAt, updatedAt, touchedAt, specJSON, runtimeJSON, statsJSON, workspaceJSON string
	err := row.Scan(
		&vm.ID,
		&vm.Name,
		&vm.ImageID,
		&vm.Runtime.GuestIP,
		&state,
		&createdAt,
		&updatedAt,
		&touchedAt,
		&specJSON,
		&runtimeJSON,
		&statsJSON,
		&workspaceJSON,
	)
	if err != nil {
		return vm, err
	}
	vm.State = model.VMState(state)
	if err := json.Unmarshal([]byte(specJSON), &vm.Spec); err != nil {
		return vm, err
	}
	if err := json.Unmarshal([]byte(runtimeJSON), &vm.Runtime); err != nil {
		return vm, err
	}
	if statsJSON != "" {
		if err := json.Unmarshal([]byte(statsJSON), &vm.Stats); err != nil {
			return vm, err
		}
	}
	if workspaceJSON != "" && workspaceJSON != "{}" {
		if err := json.Unmarshal([]byte(workspaceJSON), &vm.Workspace); err != nil {
			return vm, err
		}
	}
	var parseErr error
	vm.CreatedAt, parseErr = time.Parse(time.RFC3339, createdAt)
	if parseErr != nil {
		return vm, parseErr
	}
	vm.UpdatedAt, parseErr = time.Parse(time.RFC3339, updatedAt)
	if parseErr != nil {
		return vm, parseErr
	}
	vm.LastTouchedAt, parseErr = time.Parse(time.RFC3339, touchedAt)
	if parseErr != nil {
		return vm, parseErr
	}
	return vm, nil
}

func boolToInt(value bool) int {
	if value {
		return 1
	}
	return 0
}

func nullableTimeString(value time.Time) any {
	if value.IsZero() {
		return nil
	}
	return value.Format(time.RFC3339)
}

func nullableInt(value *int) any {
	if value == nil {
		return nil
	}
	return *value
}
