import { DatabaseSync } from 'node:sqlite';
import { chmodSync, existsSync } from 'node:fs';

const MIGRATIONS: Array<{ id: number; sql: string }> = [
  {
    id: 1,
    sql: `
      CREATE TABLE settings (
        key   TEXT PRIMARY KEY,
        value TEXT NOT NULL
      );
      CREATE TABLE auth_tokens (
        id           TEXT PRIMARY KEY,
        label        TEXT NOT NULL,
        token_hash   TEXT NOT NULL UNIQUE,
        created_at   TEXT NOT NULL,
        last_used_at TEXT,
        revoked_at   TEXT
      );
      CREATE TABLE sessions (
        id         TEXT PRIMARY KEY,
        cwd        TEXT NOT NULL,
        command    TEXT NOT NULL,
        title      TEXT,
        created_at TEXT NOT NULL,
        ended_at   TEXT,
        exit_code  INTEGER
      );
    `,
  },
  {
    // P2 : découverte & reprise : corrélation avec les sessions Claude sur disque.
    id: 2,
    sql: `
      ALTER TABLE sessions ADD COLUMN claude_session_id TEXT;
      ALTER TABLE sessions ADD COLUMN resumed_from TEXT;
      CREATE INDEX idx_sessions_claude_session_id ON sessions(claude_session_id);
    `,
  },
  {
    // P3 : repos enregistrés. Les worktrees sont dérivés à la volée de git (non persistés).
    id: 3,
    sql: `
      CREATE TABLE repos (
        id                TEXT PRIMARY KEY,
        path              TEXT NOT NULL UNIQUE,
        label             TEXT NOT NULL,
        default_branch    TEXT,
        post_create_hooks TEXT NOT NULL DEFAULT '[]',
        pre_trust         INTEGER NOT NULL DEFAULT 0,
        created_at        TEXT NOT NULL
      );
    `,
  },
  {
    // P4 : abonnements Web Push. Liés au token d'auth (token_id) ; clés VAPID en settings.
    id: 4,
    sql: `
      CREATE TABLE push_subscriptions (
        id         TEXT PRIMARY KEY,
        token_id   TEXT NOT NULL,
        endpoint   TEXT NOT NULL UNIQUE,
        p256dh     TEXT NOT NULL,
        auth       TEXT NOT NULL,
        user_agent TEXT,
        created_at TEXT NOT NULL,
        last_ok_at TEXT
      );
      CREATE INDEX idx_push_subs_token ON push_subscriptions(token_id);
    `,
  },
  {
    // P5 : groupes de travail. Un groupe = collection de repos (many-to-many).
    // Worktrees/sessions NON persistés ici : servis par WorktreeManager/PtyManager
    // et filtrés côté client par repoId. CASCADE s'appuie sur PRAGMA foreign_keys = ON (cf. openDb).
    id: 5,
    sql: `
      CREATE TABLE groups (
        id          TEXT PRIMARY KEY,
        label       TEXT NOT NULL,
        description TEXT,
        color       TEXT,
        position    INTEGER NOT NULL DEFAULT 0,
        created_at  TEXT NOT NULL,
        updated_at  TEXT NOT NULL
      );
      CREATE TABLE group_repos (
        group_id   TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
        repo_id    TEXT NOT NULL REFERENCES repos(id)  ON DELETE CASCADE,
        position   INTEGER NOT NULL DEFAULT 0,
        created_at TEXT NOT NULL,
        PRIMARY KEY (group_id, repo_id)
      );
      CREATE INDEX idx_group_repos_repo ON group_repos(repo_id);
    `,
  },
  {
    // Découverte auto : un repo masqué reste en DB (exclu du dashboard) pour qu'un
    // re-scan ne le ressuscite pas. hidden=1 = masqué.
    id: 6,
    sql: `ALTER TABLE repos ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0;`,
  },
  {
    // Journal d'audit (conformité entreprise : GDPR/SOX/ISO 27001). Trace les mutations
    // sensibles (tokens, réglages, secrets, abonnements push, groupes). Ne contient JAMAIS
    // de secret en clair, `details` est un JSON de métadonnées non sensibles.
    id: 7,
    sql: `
      CREATE TABLE audit_logs (
        id          TEXT PRIMARY KEY,
        ts          TEXT NOT NULL,
        actor       TEXT NOT NULL,
        action      TEXT NOT NULL,
        resource_id TEXT,
        details     TEXT,
        result      TEXT NOT NULL
      );
      CREATE INDEX idx_audit_ts ON audit_logs(ts);
    `,
  },
  {
    // P6 : session de groupe multi-repo. Une session peut couvrir plusieurs répertoires (--add-dir)
    // et appartenir à un groupe. `added_dirs` : JSON array de chemins absolus (NULL si mono-repo).
    // `group_id` : pas de FK (ALTER ADD COLUMN sqlite n'en pose pas) ; nettoyé à la suppression du groupe.
    id: 8,
    sql: `
      ALTER TABLE sessions ADD COLUMN added_dirs TEXT;
      ALTER TABLE sessions ADD COLUMN group_id TEXT;
      CREATE INDEX idx_sessions_group_id ON sessions(group_id);
    `,
  },
  {
    // Masquage des sessions Claude découvertes (lancées en CLI hors Arboretum) qui polluent la liste.
    // Calqué sur repos.hidden (#6) : la session masquée reste connue (resume/fork possibles) mais est
    // exclue de la liste par défaut, et un re-scan ne la ressuscite pas. Clé = claudeSessionId (stable,
    // partagé entre une découverte et sa reprise managée).
    id: 9,
    sql: `
      CREATE TABLE hidden_sessions (
        claude_session_id TEXT PRIMARY KEY,
        hidden_at         TEXT NOT NULL
      );
    `,
  },
  {
    // P10 : archivage automatique des sessions terminées (rétention configurable, défaut 30 j).
    // `archived_at` NULL = active. Soft-archive : JAMAIS de DELETE (resume/fork restent intacts).
    // Sémantique distincte de hidden_sessions (#9) : `hidden` est manuel et indexé par
    // claude_session_id ; `archived` est automatique par ancienneté, indexé par `id`, et ne
    // s'applique qu'aux sessions managées en DB. Les deux filtres sont indépendants et cumulables.
    id: 10,
    sql: `
      ALTER TABLE sessions ADD COLUMN archived_at TEXT;
      CREATE INDEX idx_sessions_archived_at ON sessions(archived_at);
    `,
  },
  {
    // P12 : connexions aux services git distants (Gitea/GitLab/GitHub). Les SECRETS (PAT, app
    // password, tokens OAuth) sont chiffrés par SecretBox AVANT insertion (colonnes *_encrypted) :
    // la base ne contient jamais de secret en clair. `ssh_key_path`/`oauth_*` sont posés dès
    // maintenant (schéma stable) mais exploités en P12b/P12c. `test_result` = dernier diagnostic.
    id: 11,
    sql: `
      CREATE TABLE git_credentials (
        id                       TEXT PRIMARY KEY,
        label                    TEXT NOT NULL,
        service                  TEXT NOT NULL CHECK (service IN ('gitea','gitlab','github')),
        base_url                 TEXT,
        auth_type                TEXT NOT NULL CHECK (auth_type IN ('pat','app_password','ssh_key','oauth')),
        username                 TEXT,
        secret_encrypted         TEXT,
        ssh_key_path             TEXT,
        oauth_access_encrypted   TEXT,
        oauth_refresh_encrypted  TEXT,
        oauth_expires_at         TEXT,
        created_at               TEXT NOT NULL,
        last_tested_at           TEXT,
        test_result              TEXT
      );
      CREATE INDEX idx_git_credentials_service ON git_credentials(service);
    `,
  },
  {
    // P12 : un repo enregistré peut provenir d'un clone : on retient sa source (remote_url),
    // le service détecté et le credential utilisé. PAS de FK sur credential_id (cohérent avec
    // sessions.group_id #8) : la suppression d'un credential nullifie ce champ côté manager.
    id: 12,
    sql: `
      ALTER TABLE repos ADD COLUMN remote_url TEXT;
      ALTER TABLE repos ADD COLUMN git_service TEXT;
      ALTER TABLE repos ADD COLUMN credential_id TEXT;
    `,
  },
];

export type Db = DatabaseSync;

export function openDb(path: string): Db {
  const db = new DatabaseSync(path);
  db.exec('PRAGMA journal_mode = WAL');
  db.exec('PRAGMA foreign_keys = ON');
  hardenDbPermissions(path);
  migrate(db);
  return db;
}

/**
 * Restreint la base (et ses fichiers WAL/SHM) à 0o600, proprio uniquement. La base contient des
 * secrets (server_secret, clé privée VAPID, hashs de tokens) : elle ne doit jamais être lisible par
 * d'autres utilisateurs du système. Best-effort : ignoré sur les FS sans permissions POSIX.
 */
function hardenDbPermissions(path: string): void {
  if (path === ':memory:') return;
  for (const p of [path, `${path}-wal`, `${path}-shm`]) {
    try {
      if (existsSync(p)) chmodSync(p, 0o600);
    } catch {
      /* FS sans permissions POSIX (Windows / montage) : ignoré */
    }
  }
}

function migrate(db: DatabaseSync): void {
  db.exec('CREATE TABLE IF NOT EXISTS schema_migrations (id INTEGER PRIMARY KEY, applied_at TEXT NOT NULL)');
  const applied = new Set(
    (db.prepare('SELECT id FROM schema_migrations').all() as Array<{ id: number }>).map((r) => r.id),
  );
  for (const m of MIGRATIONS) {
    if (applied.has(m.id)) continue;
    db.exec('BEGIN');
    try {
      db.exec(m.sql);
      db.prepare('INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)').run(m.id, new Date().toISOString());
      db.exec('COMMIT');
    } catch (err) {
      db.exec('ROLLBACK');
      throw err;
    }
  }
}

export function getSetting(db: Db, key: string): string | null {
  const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
  return row?.value ?? null;
}

export function setSetting(db: Db, key: string, value: string): void {
  db.prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);
}

// ---- Sessions masquées (par claudeSessionId) ----

/** Ensemble des claudeSessionId masqués par l'utilisateur. */
export function listHiddenSessionIds(db: Db): Set<string> {
  const rows = db.prepare('SELECT claude_session_id FROM hidden_sessions').all() as Array<{ claude_session_id: string }>;
  return new Set(rows.map((r) => r.claude_session_id));
}

/** Masque une session (idempotent). */
export function hideSession(db: Db, claudeSessionId: string): void {
  db.prepare(
    'INSERT INTO hidden_sessions (claude_session_id, hidden_at) VALUES (?, ?) ON CONFLICT(claude_session_id) DO NOTHING',
  ).run(claudeSessionId, new Date().toISOString());
}

/** Ré-affiche une session masquée (idempotent). */
export function unhideSession(db: Db, claudeSessionId: string): void {
  db.prepare('DELETE FROM hidden_sessions WHERE claude_session_id = ?').run(claudeSessionId);
}

// ---- Archivage des sessions terminées (par id, soft-archive, P10) ----

/** Archive une session managée (idempotent : ne touche pas une session déjà archivée). */
export function archiveSession(db: Db, id: string, archivedAt: string): void {
  db.prepare('UPDATE sessions SET archived_at = ? WHERE id = ? AND archived_at IS NULL').run(archivedAt, id);
}

/** Dés-archive une session (idempotent). La session reste reprenable/forkable (inchangé). */
export function unarchiveSession(db: Db, id: string): void {
  db.prepare('UPDATE sessions SET archived_at = NULL WHERE id = ?').run(id);
}

/** true si une session managée avec cet id existe en DB. */
export function sessionExists(db: Db, id: string): boolean {
  return db.prepare('SELECT 1 FROM sessions WHERE id = ?').get(id) != null;
}

/** true si la session existe en DB et est archivée. */
export function isSessionArchived(db: Db, id: string): boolean {
  const row = db.prepare('SELECT archived_at FROM sessions WHERE id = ?').get(id) as { archived_at: string | null } | undefined;
  return row != null && row.archived_at != null;
}

/**
 * Archive en masse les sessions terminées plus anciennes que `cutoffIso` (comparaison lexicographique
 * sur ended_at, format ISO UTC). Retourne les `id` archivés (pour émettre les events + auditer).
 * SELECT puis UPDATE dans une transaction → la liste retournée correspond exactement aux lignes mutées.
 * Les sessions vivantes (ended_at NULL) et déjà archivées sont exclues.
 */
export function archiveExpiredSessions(db: Db, archivedAt: string, cutoffIso: string): string[] {
  db.exec('BEGIN');
  try {
    const rows = db
      .prepare('SELECT id FROM sessions WHERE ended_at IS NOT NULL AND ended_at < ? AND archived_at IS NULL')
      .all(cutoffIso) as Array<{ id: string }>;
    const stmt = db.prepare('UPDATE sessions SET archived_at = ? WHERE id = ?');
    for (const r of rows) stmt.run(archivedAt, r.id);
    db.exec('COMMIT');
    return rows.map((r) => r.id);
  } catch (err) {
    db.exec('ROLLBACK');
    throw err;
  }
}
