// Package db gère la connexion SQLite et l'exécution des migrations.
// Il expose une instance unique de base de données utilisée par tous les services.
package db

import (
	"database/sql"
	"embed"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"

	_ "modernc.org/sqlite" // Pilote SQLite pur Go (sans CGO)
)

//go:embed migrations/*.sql
var migrationsFS embed.FS

// DB encapsule la connexion SQLite et expose les méthodes nécessaires.
type DB struct {
	*sql.DB
}

// Open ouvre (ou crée) la base de données SQLite au chemin donné et exécute les migrations.
func Open(dataDir string) (*DB, error) {
	if err := os.MkdirAll(dataDir, 0700); err != nil {
		return nil, fmt.Errorf("création répertoire données : %w", err)
	}

	dbPath := filepath.Join(dataDir, "panel.db")

	// Paramètres SQLite : WAL mode pour les lectures concurrentes, foreign keys activées
	dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(5000)", dbPath)
	sqlDB, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, fmt.Errorf("ouverture SQLite : %w", err)
	}

	// Limiter les connexions simultanées (SQLite n'est pas conçu pour la concurrence élevée)
	sqlDB.SetMaxOpenConns(1)
	sqlDB.SetMaxIdleConns(1)

	if err := sqlDB.Ping(); err != nil {
		return nil, fmt.Errorf("connexion SQLite : %w", err)
	}

	db := &DB{sqlDB}

	// Exécuter les migrations manquantes
	if err := db.migrate(); err != nil {
		return nil, fmt.Errorf("migrations : %w", err)
	}

	// Réparer les colonnes manquantes (bases créées avant le fix multi-statements)
	if err := db.repairSchema(); err != nil {
		return nil, fmt.Errorf("réparation schéma : %w", err)
	}

	return db, nil
}

// migrate applique les fichiers SQL de migration non encore exécutés.
// Les fichiers sont numérotés (001_init.sql, 002_xxx.sql) et appliqués dans l'ordre.
func (db *DB) migrate() error {
	// Créer la table schema_version si elle n'existe pas encore
	// (nécessaire avant de lire la version actuelle)
	_, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_version (
		version INTEGER NOT NULL,
		applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
	)`)
	if err != nil {
		return fmt.Errorf("création schema_version : %w", err)
	}

	// Lire la version actuelle
	var currentVersion int
	row := db.QueryRow(`SELECT COALESCE(MAX(version), 0) FROM schema_version`)
	if err := row.Scan(&currentVersion); err != nil {
		return fmt.Errorf("lecture version schéma : %w", err)
	}

	// Lister et trier les fichiers de migration
	entries, err := migrationsFS.ReadDir("migrations")
	if err != nil {
		return fmt.Errorf("lecture dossier migrations : %w", err)
	}

	type migration struct {
		version int
		name    string
	}
	var migrations []migration

	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
			continue
		}
		// Extraire le numéro de version depuis le nom du fichier (ex: "001_init.sql" → 1)
		parts := strings.SplitN(entry.Name(), "_", 2)
		if len(parts) < 1 {
			continue
		}
		v, err := strconv.Atoi(parts[0])
		if err != nil {
			continue
		}
		migrations = append(migrations, migration{version: v, name: entry.Name()})
	}

	// Trier par numéro de version croissant
	sort.Slice(migrations, func(i, j int) bool {
		return migrations[i].version < migrations[j].version
	})

	// Appliquer les migrations manquantes
	for _, m := range migrations {
		if m.version <= currentVersion {
			continue
		}

		content, err := migrationsFS.ReadFile("migrations/" + m.name)
		if err != nil {
			return fmt.Errorf("lecture migration %s : %w", m.name, err)
		}

		// Exécuter dans une transaction pour garantir l'atomicité
		tx, err := db.Begin()
		if err != nil {
			return fmt.Errorf("transaction migration %s : %w", m.name, err)
		}

		// Splitter par ";" pour exécuter chaque statement séparément
		// (SQLite / database/sql n'exécute qu'un seul statement par Exec)
		for _, stmt := range strings.Split(string(content), ";") {
			stmt = strings.TrimSpace(stmt)
			if stmt == "" {
				continue
			}
			if _, err := tx.Exec(stmt); err != nil {
				tx.Rollback()
				return fmt.Errorf("exécution migration %s : %w", m.name, err)
			}
		}

		// Mettre à jour la version (la migration 001 l'insère elle-même, pas besoin de le refaire)
		if m.version > 1 {
			if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES (?)`, m.version); err != nil {
				tx.Rollback()
				return fmt.Errorf("mise à jour version après migration %s : %w", m.name, err)
			}
		}

		if err := tx.Commit(); err != nil {
			return fmt.Errorf("commit migration %s : %w", m.name, err)
		}
	}

	return nil
}

// GetSetting lit un paramètre depuis la table settings.
// Retourne "" et nil si la clé n'existe pas.
func (db *DB) GetSetting(key string) (string, bool, error) {
	var value string
	var encrypted int
	err := db.QueryRow(`SELECT value, encrypted FROM settings WHERE key = ?`, key).Scan(&value, &encrypted)
	if err == sql.ErrNoRows {
		return "", false, nil
	}
	if err != nil {
		return "", false, err
	}
	return value, encrypted == 1, nil
}

// SetSetting enregistre ou met à jour un paramètre.
func (db *DB) SetSetting(key, value string, encrypted bool) error {
	enc := 0
	if encrypted {
		enc = 1
	}
	_, err := db.Exec(`
		INSERT INTO settings (key, value, encrypted, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)
		ON CONFLICT(key) DO UPDATE SET value=excluded.value, encrypted=excluded.encrypted, updated_at=excluded.updated_at
	`, key, value, enc)
	return err
}

// IsInstalled vérifie si l'application a déjà été configurée.
func (db *DB) IsInstalled() (bool, error) {
	v, _, err := db.GetSetting("installed")
	if err != nil {
		return false, err
	}
	return v == "true", nil
}

// repairSchema ajoute les colonnes manquantes dans les bases créées avant le fix
// multi-statements des migrations. Migration 002 était partiellement appliquée
// (seul user_agent ajouté) sur les bases existantes.
func (db *DB) repairSchema() error {
	type col struct {
		table, name, def string
	}
	needed := []col{
		{"refresh_tokens", "user_agent", "TEXT NOT NULL DEFAULT ''"},
		{"refresh_tokens", "ip", "TEXT NOT NULL DEFAULT ''"},
		{"refresh_tokens", "last_used_at", "DATETIME"},
		// Migration 005 : colonnes de navigation des modules (ajout idempotent)
		{"modules", "nav_href", "TEXT NOT NULL DEFAULT ''"},
		{"modules", "nav_icon", "TEXT NOT NULL DEFAULT ''"},
		{"modules", "nav_color", "TEXT NOT NULL DEFAULT ''"},
		{"modules", "nav_label_key", "TEXT NOT NULL DEFAULT ''"},
		{"modules", "repo_url", "TEXT NOT NULL DEFAULT ''"},
	}
	for _, c := range needed {
		if err := db.ensureColumn(c.table, c.name, c.def); err != nil {
			return err
		}
	}
	return nil
}

// ensureColumn ajoute une colonne à une table si elle n'existe pas déjà.
func (db *DB) ensureColumn(table, column, definition string) error {
	rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
	if err != nil {
		return fmt.Errorf("PRAGMA table_info(%s) : %w", table, err)
	}
	defer rows.Close()

	for rows.Next() {
		var cid int
		var name, colType string
		var notNull, pk int
		var dflt sql.NullString
		if err := rows.Scan(&cid, &name, &colType, &notNull, &dflt, &pk); err != nil {
			return err
		}
		if name == column {
			return nil // déjà présente
		}
	}
	if err := rows.Err(); err != nil {
		return err
	}

	_, err = db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, definition))
	return err
}
