package database

import (
	"database/sql"
	"fmt"
	"os"
	"path/filepath"

	_ "github.com/mattn/go-sqlite3"
)

type DB struct {
	*sql.DB
}

// New opens (or creates) the SQLite database, enables WAL mode, and runs migrations.
func New(dbPath string) (*DB, error) {
	dir := filepath.Dir(dbPath)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, fmt.Errorf("create db directory: %w", err)
	}

	db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on")
	if err != nil {
		return nil, fmt.Errorf("open database: %w", err)
	}

	if err := db.Ping(); err != nil {
		return nil, fmt.Errorf("ping database: %w", err)
	}

	// Enforce WAL mode explicitly
	if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
		return nil, fmt.Errorf("set WAL mode: %w", err)
	}

	wrapped := &DB{db}
	if err := wrapped.migrate(); err != nil {
		return nil, fmt.Errorf("run migrations: %w", err)
	}

	return wrapped, nil
}

func (db *DB) migrate() error {
	migrations := []string{
		// Users table – single user now, designed for future multi-user
		`CREATE TABLE IF NOT EXISTS users (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			username TEXT NOT NULL UNIQUE,
			password_hash TEXT NOT NULL,
			totp_secret TEXT DEFAULT '',
			totp_enabled INTEGER DEFAULT 0,
			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
		)`,

		// Files metadata table
		`CREATE TABLE IF NOT EXISTS files (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			path TEXT NOT NULL UNIQUE,
			name TEXT NOT NULL,
			is_dir INTEGER DEFAULT 0,
			size INTEGER DEFAULT 0,
			mime_type TEXT DEFAULT '',
			checksum TEXT DEFAULT '',
			is_pinned INTEGER DEFAULT 0,
			is_trashed INTEGER DEFAULT 0,
			trashed_at DATETIME,
			original_path TEXT DEFAULT '',
			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
		)`,

		// Index for fast trash and pin queries
		`CREATE INDEX IF NOT EXISTS idx_files_trashed ON files(is_trashed)`,
		`CREATE INDEX IF NOT EXISTS idx_files_pinned ON files(is_pinned)`,
		`CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)`,

		// File versions table
		`CREATE TABLE IF NOT EXISTS file_versions (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			file_path TEXT NOT NULL,
			version_number INTEGER NOT NULL,
			version_path TEXT NOT NULL,
			size INTEGER DEFAULT 0,
			checksum TEXT DEFAULT '',
			created_at DATETIME DEFAULT CURRENT_TIMESTAMP
		)`,

		// Share links table
		`CREATE TABLE IF NOT EXISTS share_links (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			token TEXT NOT NULL UNIQUE,
			target_path TEXT NOT NULL,
			is_dir INTEGER DEFAULT 0,
			password_hash TEXT DEFAULT '',
			expires_at DATETIME,
			max_downloads INTEGER DEFAULT 0,
			download_count INTEGER DEFAULT 0,
			created_at DATETIME DEFAULT CURRENT_TIMESTAMP
		)`,
		`CREATE INDEX IF NOT EXISTS idx_share_links_token ON share_links(token)`,

		// Audit log table
		`CREATE TABLE IF NOT EXISTS audit_log (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			action TEXT NOT NULL,
			details TEXT DEFAULT '',
			ip_address TEXT DEFAULT '',
			created_at DATETIME DEFAULT CURRENT_TIMESTAMP
		)`,

		// Settings table (key-value)
		`CREATE TABLE IF NOT EXISTS settings (
			key TEXT PRIMARY KEY,
			value TEXT DEFAULT ''
		)`,

		// FTS5 virtual table for file search
		`CREATE VIRTUAL TABLE IF NOT EXISTS files_fts USING fts5(
			name,
			path,
			content=files,
			content_rowid=id
		)`,

		// Triggers to keep FTS5 in sync with the files table
		`CREATE TRIGGER IF NOT EXISTS files_ai AFTER INSERT ON files BEGIN
			INSERT INTO files_fts(rowid, name, path) VALUES (new.id, new.name, new.path);
		END`,

		`CREATE TRIGGER IF NOT EXISTS files_ad AFTER DELETE ON files BEGIN
			INSERT INTO files_fts(files_fts, rowid, name, path) VALUES('delete', old.id, old.name, old.path);
		END`,

		`CREATE TRIGGER IF NOT EXISTS files_au AFTER UPDATE ON files BEGIN
			INSERT INTO files_fts(files_fts, rowid, name, path) VALUES('delete', old.id, old.name, old.path);
			INSERT INTO files_fts(rowid, name, path) VALUES (new.id, new.name, new.path);
		END`,

		// Default settings
		`INSERT OR IGNORE INTO settings (key, value) VALUES ('theme', 'dark')`,
		`INSERT OR IGNORE INTO settings (key, value) VALUES ('thumbnail_images', 'true')`,
		`INSERT OR IGNORE INTO settings (key, value) VALUES ('thumbnail_videos', 'true')`,
		`INSERT OR IGNORE INTO settings (key, value) VALUES ('max_versions', '5')`,
		`INSERT OR IGNORE INTO settings (key, value) VALUES ('trash_auto_purge_days', '30')`,
	}

	for _, m := range migrations {
		if _, err := db.Exec(m); err != nil {
			return fmt.Errorf("migration error: %w\nSQL: %s", err, m)
		}
	}

	return nil
}

// IsSetupComplete checks if any user account exists.
func (db *DB) IsSetupComplete() (bool, error) {
	var count int
	err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
	if err != nil {
		return false, err
	}
	return count > 0, nil
}

// GetSetting retrieves a setting value by key.
func (db *DB) GetSetting(key string) (string, error) {
	var value string
	err := db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value)
	return value, err
}

// SetSetting upserts a setting.
func (db *DB) SetSetting(key, value string) error {
	_, err := db.Exec("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", key, value)
	return err
}

// AddAuditLog appends an entry to the audit log.
func (db *DB) AddAuditLog(action, details, ip string) error {
	_, err := db.Exec("INSERT INTO audit_log (action, details, ip_address) VALUES (?, ?, ?)", action, details, ip)
	return err
}
