package db

import (
	"context"
	"database/sql"
	_ "embed"
	"fmt"
	"net/url"

	_ "modernc.org/sqlite"
)

//go:embed migrations/0001_init.sql
var initSchema string

// Open dials a sqlite db with WAL + busy timeout pragmas wired in.
// dbPath is the on-disk file path; passing ":memory:" works for tests.
func Open(dbPath string) (*sql.DB, error) {
	dsn := buildDSN(dbPath)
	d, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, fmt.Errorf("open sqlite: %w", err)
	}
	if err := d.PingContext(context.Background()); err != nil {
		_ = d.Close()
		return nil, fmt.Errorf("ping sqlite: %w", err)
	}
	return d, nil
}

func buildDSN(dbPath string) string {
	q := url.Values{}
	q.Add("_pragma", "journal_mode(WAL)")
	q.Add("_pragma", "busy_timeout(5000)")
	q.Add("_pragma", "foreign_keys(1)")
	return dbPath + "?" + q.Encode()
}

// Bootstrap runs the embedded init schema in a single transaction.
// Idempotent: every CREATE TABLE uses IF NOT EXISTS.
func Bootstrap(ctx context.Context, d *sql.DB) error {
	tx, err := d.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("begin bootstrap tx: %w", err)
	}
	defer func() { _ = tx.Rollback() }()

	if _, err := tx.ExecContext(ctx, initSchema); err != nil {
		return fmt.Errorf("exec init schema: %w", err)
	}
	// Additive migration for DBs created before clipboard_state.version existed:
	// CREATE TABLE IF NOT EXISTS won't add the column to an existing table, and
	// SQLite has no ADD COLUMN IF NOT EXISTS, so guard it with a pragma check.
	if err := ensureColumn(ctx, tx, "clipboard_state", "version",
		"ALTER TABLE clipboard_state ADD COLUMN version INTEGER NOT NULL DEFAULT 0"); err != nil {
		return fmt.Errorf("migrate clipboard_state.version: %w", err)
	}
	if err := ensureColumn(ctx, tx, "clipboard_state", "origin_ts",
		"ALTER TABLE clipboard_state ADD COLUMN origin_ts INTEGER NOT NULL DEFAULT 0"); err != nil {
		return fmt.Errorf("migrate clipboard_state.origin_ts: %w", err)
	}
	// web_sessions gained kind/scope/granted_by for cdrop self-signed sessions
	// (AUTH.md §2.2). Existing rows predate these columns; backfill them so a
	// pre-existing browser session keeps behaving as a normal IdP-backed login.
	if err := ensureColumn(ctx, tx, "web_sessions", "kind",
		"ALTER TABLE web_sessions ADD COLUMN kind TEXT NOT NULL DEFAULT 'oidc'"); err != nil {
		return fmt.Errorf("migrate web_sessions.kind: %w", err)
	}
	if err := ensureColumn(ctx, tx, "web_sessions", "scope",
		"ALTER TABLE web_sessions ADD COLUMN scope TEXT NOT NULL DEFAULT 'full'"); err != nil {
		return fmt.Errorf("migrate web_sessions.scope: %w", err)
	}
	if err := ensureColumn(ctx, tx, "web_sessions", "granted_by",
		"ALTER TABLE web_sessions ADD COLUMN granted_by TEXT NOT NULL DEFAULT ''"); err != nil {
		return fmt.Errorf("migrate web_sessions.granted_by: %w", err)
	}
	if err := ensureColumn(ctx, tx, "web_sessions", "stepped_up_at",
		"ALTER TABLE web_sessions ADD COLUMN stepped_up_at INTEGER NOT NULL DEFAULT 0"); err != nil {
		return fmt.Errorf("migrate web_sessions.stepped_up_at: %w", err)
	}
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("commit bootstrap: %w", err)
	}
	return nil
}

// ensureColumn runs addSQL only when table lacks the named column — an idempotent
// additive migration for already-created tables.
func ensureColumn(ctx context.Context, tx *sql.Tx, table, column, addSQL string) error {
	has, err := columnExists(ctx, tx, table, column)
	if err != nil {
		return err
	}
	if has {
		return nil
	}
	_, err = tx.ExecContext(ctx, addSQL)
	return err
}

func columnExists(ctx context.Context, tx *sql.Tx, table, column string) (bool, error) {
	// table is a trusted compile-time constant, not user input.
	rows, err := tx.QueryContext(ctx, "PRAGMA table_info("+table+")")
	if err != nil {
		return false, err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			cid, notnull, pk int
			name, ctype      string
			dflt             sql.NullString
		)
		if err := rows.Scan(&cid, &name, &ctype, &notnull, &dflt, &pk); err != nil {
			return false, err
		}
		if name == column {
			return true, nil
		}
	}
	return false, rows.Err()
}
