package store

import (
	"database/sql"
	"errors"
	"fmt"
	"time"

	_ "modernc.org/sqlite"
)

// ErrNotFound is returned when a requested entity does not exist.
var ErrNotFound = errors.New("not found")

// Store wraps the SQLite database connection and provides access to all query methods.
type Store struct {
	db *sql.DB
}

// New opens a SQLite database at the given path and runs auto-migration.
func New(dbPath string) (*Store, error) {
	db, err := sql.Open("sqlite", dbPath)
	if err != nil {
		return nil, fmt.Errorf("open database: %w", err)
	}

	// SQLite only allows one writer at a time. Limit connections to prevent SQLITE_BUSY.
	db.SetMaxOpenConns(1)
	db.SetConnMaxLifetime(0)

	// Enable WAL mode and foreign keys for better concurrency and referential integrity.
	pragmas := []string{
		"PRAGMA journal_mode=WAL",
		"PRAGMA foreign_keys=ON",
		"PRAGMA busy_timeout=5000",
	}
	for _, p := range pragmas {
		if _, err := db.Exec(p); err != nil {
			db.Close()
			return nil, fmt.Errorf("exec pragma %q: %w", p, err)
		}
	}

	s := &Store{db: db}
	if err := s.migrate(); err != nil {
		db.Close()
		return nil, fmt.Errorf("migrate: %w", err)
	}

	return s, nil
}

// Close closes the underlying database connection.
func (s *Store) Close() error {
	return s.db.Close()
}

// DB returns the underlying *sql.DB for advanced operations like transactions.
func (s *Store) DB() *sql.DB {
	return s.db
}

// migrate creates all tables if they do not already exist, then runs
// incremental migrations for schema changes added after initial release.
func (s *Store) migrate() error {
	if _, err := s.db.Exec(schema); err != nil {
		return err
	}
	return s.runMigrations()
}

// runMigrations applies additive schema changes that cannot be expressed
// with CREATE TABLE IF NOT EXISTS.
func (s *Store) runMigrations() error {
	migrations := []string{
		// Add owner column to registries (2026-03-28).
		`ALTER TABLE registries ADD COLUMN owner TEXT NOT NULL DEFAULT ''`,
		// Add base_volume_path to settings (2026-03-28).
		`ALTER TABLE settings ADD COLUMN base_volume_path TEXT NOT NULL DEFAULT ''`,
		// Add enable_proxy to stages (2026-03-29). Default true for backwards compat.
		`ALTER TABLE stages ADD COLUMN enable_proxy INTEGER NOT NULL DEFAULT 1`,
		// Add ssl_certificate_id to settings (2026-03-29).
		`ALTER TABLE settings ADD COLUMN ssl_certificate_id INTEGER NOT NULL DEFAULT 0`,
		// Add stale_threshold_days to settings (2026-03-30).
		`ALTER TABLE settings ADD COLUMN stale_threshold_days INTEGER NOT NULL DEFAULT 7`,
		// Add last_alive_at to instances for stale container detection (2026-03-30).
		`ALTER TABLE instances ADD COLUMN last_alive_at TEXT NOT NULL DEFAULT ''`,
		// Add name column and rename mode→scope for volume scopes redesign (2026-03-31).
		`ALTER TABLE volumes ADD COLUMN name TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE volumes ADD COLUMN scope TEXT NOT NULL DEFAULT ''`,
		// Add allowed_volume_paths to settings for absolute volume scope allowlist (2026-04-01).
		`ALTER TABLE settings ADD COLUMN allowed_volume_paths TEXT NOT NULL DEFAULT '[]'`,
		// Add DNS management fields to settings (2026-04-02).
		`ALTER TABLE settings ADD COLUMN wildcard_dns INTEGER NOT NULL DEFAULT 1`,
		`ALTER TABLE settings ADD COLUMN dns_provider TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE settings ADD COLUMN cloudflare_api_token TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE settings ADD COLUMN cloudflare_zone_id TEXT NOT NULL DEFAULT ''`,
		// Add backup management fields to settings (2026-04-02).
		`ALTER TABLE settings ADD COLUMN backup_enabled INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE settings ADD COLUMN backup_interval_hours INTEGER NOT NULL DEFAULT 24`,
		`ALTER TABLE settings ADD COLUMN backup_retention_count INTEGER NOT NULL DEFAULT 10`,
		`ALTER TABLE stages ADD COLUMN notification_url TEXT NOT NULL DEFAULT ''`,
		// Add proxy_route_id to instances for provider-agnostic route tracking (2026-04-04).
		`ALTER TABLE instances ADD COLUMN proxy_route_id TEXT NOT NULL DEFAULT ''`,
		// Add proxy_provider to settings (2026-04-04). Default to npm for backward compat.
		`ALTER TABLE settings ADD COLUMN proxy_provider TEXT NOT NULL DEFAULT 'npm'`,
		// Add Traefik provider settings (2026-04-04).
		`ALTER TABLE settings ADD COLUMN traefik_entrypoint TEXT NOT NULL DEFAULT 'websecure'`,
		`ALTER TABLE settings ADD COLUMN traefik_cert_resolver TEXT NOT NULL DEFAULT 'letsencrypt'`,
		`ALTER TABLE settings ADD COLUMN traefik_network TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE settings ADD COLUMN traefik_api_url TEXT NOT NULL DEFAULT ''`,
	}

	for _, m := range migrations {
		// Ignore errors from already-applied migrations (duplicate column).
		_, _ = s.db.Exec(m)
	}

	// Create indexes on foreign key columns for query performance.
	indexes := []string{
		`CREATE INDEX IF NOT EXISTS idx_instances_stage_id ON instances(stage_id)`,
		`CREATE INDEX IF NOT EXISTS idx_instances_project_id ON instances(project_id)`,
		`CREATE INDEX IF NOT EXISTS idx_deploys_project_id ON deploys(project_id)`,
		`CREATE INDEX IF NOT EXISTS idx_deploys_stage_id ON deploys(stage_id)`,
		`CREATE INDEX IF NOT EXISTS idx_deploy_logs_deploy_id ON deploy_logs(deploy_id)`,
		`CREATE INDEX IF NOT EXISTS idx_stages_project_id ON stages(project_id)`,
		`CREATE INDEX IF NOT EXISTS idx_stage_env_stage_id ON stage_env(stage_id)`,
		`CREATE INDEX IF NOT EXISTS idx_volumes_project_id ON volumes(project_id)`,
		`CREATE INDEX IF NOT EXISTS idx_event_log_severity ON event_log(severity)`,
		`CREATE INDEX IF NOT EXISTS idx_event_log_source ON event_log(source)`,
		`CREATE INDEX IF NOT EXISTS idx_event_log_created_at ON event_log(created_at)`,
		`CREATE INDEX IF NOT EXISTS idx_dns_records_consumer ON dns_records(consumer_type, consumer_id)`,
	}
	for _, idx := range indexes {
		if _, err := s.db.Exec(idx); err != nil {
			return fmt.Errorf("create index: %w", err)
		}
	}

	// Data migration: copy mode→scope for volumes that have scope still empty.
	// shared→project, isolated→instance. Log errors but don't fail startup.
	dataMigrations := []struct{ query, desc string }{
		{`UPDATE volumes SET scope = 'project' WHERE scope = '' AND mode = 'shared'`, "migrate shared→project"},
		{`UPDATE volumes SET scope = 'instance' WHERE scope = '' AND mode = 'isolated'`, "migrate isolated→instance"},
		{`UPDATE volumes SET scope = 'project' WHERE scope = '' AND mode = ''`, "migrate empty→project"},
	}
	for _, dm := range dataMigrations {
		if _, err := s.db.Exec(dm.query); err != nil {
			fmt.Printf("volume scope migration warning (%s): %v\n", dm.desc, err)
		}
	}

	return nil
}

const schema = `
CREATE TABLE IF NOT EXISTS projects (
	id         TEXT PRIMARY KEY,
	name       TEXT NOT NULL UNIQUE,
	registry   TEXT NOT NULL DEFAULT '',
	image      TEXT NOT NULL,
	port       INTEGER NOT NULL DEFAULT 0,
	healthcheck TEXT NOT NULL DEFAULT '',
	env        TEXT NOT NULL DEFAULT '{}',
	volumes    TEXT NOT NULL DEFAULT '{}',
	created_at TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS stages (
	id            TEXT PRIMARY KEY,
	project_id    TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
	name          TEXT NOT NULL,
	tag_pattern   TEXT NOT NULL DEFAULT '*',
	auto_deploy   INTEGER NOT NULL DEFAULT 0,
	max_instances INTEGER NOT NULL DEFAULT 1,
	confirm       INTEGER NOT NULL DEFAULT 0,
	enable_proxy  INTEGER NOT NULL DEFAULT 1,
	promote_from  TEXT NOT NULL DEFAULT '',
	subdomain         TEXT NOT NULL DEFAULT '',
	notification_url  TEXT NOT NULL DEFAULT '',
	created_at    TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at    TEXT NOT NULL DEFAULT (datetime('now')),
	UNIQUE(project_id, name)
);

CREATE TABLE IF NOT EXISTS registries (
	id         TEXT PRIMARY KEY,
	name       TEXT NOT NULL UNIQUE,
	url        TEXT NOT NULL,
	type       TEXT NOT NULL DEFAULT 'generic',
	token      TEXT NOT NULL DEFAULT '',
	created_at TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS settings (
	id                  INTEGER PRIMARY KEY CHECK (id = 1),
	domain              TEXT NOT NULL DEFAULT '',
	server_ip           TEXT NOT NULL DEFAULT '',
	network             TEXT NOT NULL DEFAULT '',
	subdomain_pattern   TEXT NOT NULL DEFAULT 'stage-{stage}-{project}',
	notification_url    TEXT NOT NULL DEFAULT '',
	npm_url             TEXT NOT NULL DEFAULT '',
	npm_email           TEXT NOT NULL DEFAULT '',
	npm_password        TEXT NOT NULL DEFAULT '',
	webhook_secret      TEXT NOT NULL DEFAULT '',
	polling_interval    TEXT NOT NULL DEFAULT '5m',
	base_volume_path   TEXT NOT NULL DEFAULT '',
	ssl_certificate_id     INTEGER NOT NULL DEFAULT 0,
	traefik_entrypoint     TEXT NOT NULL DEFAULT 'websecure',
	traefik_cert_resolver  TEXT NOT NULL DEFAULT 'letsencrypt',
	traefik_network        TEXT NOT NULL DEFAULT '',
	traefik_api_url        TEXT NOT NULL DEFAULT '',
	updated_at             TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS instances (
	id            TEXT PRIMARY KEY,
	stage_id      TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
	project_id    TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
	container_id  TEXT NOT NULL DEFAULT '',
	image_tag     TEXT NOT NULL,
	subdomain     TEXT NOT NULL DEFAULT '',
	npm_proxy_id  INTEGER NOT NULL DEFAULT 0,
	status        TEXT NOT NULL DEFAULT 'stopped',
	port          INTEGER NOT NULL DEFAULT 0,
	created_at    TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS deploys (
	id          TEXT PRIMARY KEY,
	project_id  TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
	stage_id    TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
	instance_id TEXT NOT NULL DEFAULT '',
	image_tag   TEXT NOT NULL,
	status      TEXT NOT NULL DEFAULT 'pending',
	started_at  TEXT NOT NULL DEFAULT (datetime('now')),
	finished_at TEXT NOT NULL DEFAULT '',
	error       TEXT NOT NULL DEFAULT ''
);

CREATE TABLE IF NOT EXISTS deploy_logs (
	id         INTEGER PRIMARY KEY AUTOINCREMENT,
	deploy_id  TEXT NOT NULL REFERENCES deploys(id) ON DELETE CASCADE,
	message    TEXT NOT NULL,
	level      TEXT NOT NULL DEFAULT 'info',
	created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS poll_states (
	stage_id    TEXT PRIMARY KEY REFERENCES stages(id) ON DELETE CASCADE,
	last_tag    TEXT NOT NULL DEFAULT '',
	last_polled TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS users (
	id            TEXT PRIMARY KEY,
	username      TEXT NOT NULL UNIQUE,
	password_hash TEXT NOT NULL DEFAULT '',
	email         TEXT NOT NULL DEFAULT '',
	role          TEXT NOT NULL DEFAULT 'viewer',
	created_at    TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS auth_settings (
	id                  INTEGER PRIMARY KEY CHECK (id = 1),
	auth_mode           TEXT NOT NULL DEFAULT 'local',
	oidc_client_id      TEXT NOT NULL DEFAULT '',
	oidc_client_secret  TEXT NOT NULL DEFAULT '',
	oidc_issuer_url     TEXT NOT NULL DEFAULT '',
	oidc_redirect_url   TEXT NOT NULL DEFAULT ''
);

-- Seed the settings row if it does not exist.
INSERT OR IGNORE INTO settings (id) VALUES (1);

-- Seed the auth_settings row if it does not exist.
INSERT OR IGNORE INTO auth_settings (id) VALUES (1);

CREATE TABLE IF NOT EXISTS stage_env (
	id         TEXT PRIMARY KEY,
	stage_id   TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
	key        TEXT NOT NULL,
	value      TEXT NOT NULL DEFAULT '',
	encrypted  INTEGER NOT NULL DEFAULT 0,
	created_at TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at TEXT NOT NULL DEFAULT (datetime('now')),
	UNIQUE(stage_id, key)
);

CREATE TABLE IF NOT EXISTS volumes (
	id         TEXT PRIMARY KEY,
	project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
	source     TEXT NOT NULL,
	target     TEXT NOT NULL,
	mode       TEXT NOT NULL DEFAULT 'shared',
	created_at TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS event_log (
	id         INTEGER PRIMARY KEY AUTOINCREMENT,
	source     TEXT NOT NULL DEFAULT '',
	severity   TEXT NOT NULL DEFAULT 'info',
	message    TEXT NOT NULL DEFAULT '',
	metadata   TEXT NOT NULL DEFAULT '{}',
	created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS standalone_proxies (
	id                TEXT PRIMARY KEY,
	domain            TEXT NOT NULL UNIQUE,
	destination_url   TEXT NOT NULL DEFAULT '',
	destination_port  INTEGER NOT NULL DEFAULT 0,
	ssl_certificate_id INTEGER NOT NULL DEFAULT 0,
	npm_proxy_id      INTEGER NOT NULL DEFAULT 0,
	health_status     TEXT NOT NULL DEFAULT 'unknown',
	health_checked_at TEXT NOT NULL DEFAULT '',
	created_at        TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at        TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS dns_records (
	id                 TEXT PRIMARY KEY,
	fqdn               TEXT NOT NULL UNIQUE,
	provider_record_id TEXT NOT NULL DEFAULT '',
	consumer_type      TEXT NOT NULL DEFAULT '',
	consumer_id        TEXT NOT NULL DEFAULT '',
	created_at         TEXT NOT NULL DEFAULT (datetime('now')),
	updated_at         TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS backups (
	id          TEXT PRIMARY KEY,
	filename    TEXT NOT NULL UNIQUE,
	size_bytes  INTEGER NOT NULL DEFAULT 0,
	backup_type TEXT NOT NULL DEFAULT 'manual',
	created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);
`

// Now returns the current time formatted for SQLite storage.
func Now() string {
	return time.Now().UTC().Format("2006-01-02 15:04:05")
}
