#!/usr/bin/env node
// =============================================================================
// AgenciaPsi — Database CLI
// =============================================================================
// Uso: node db.cjs <comando> [opcoes]
//
// Comandos:
//   setup              Instalação do zero (schema + fixes + seeds + migrations)
//   backup             Exporta backup com data atual (+ supabase_restore.sql)
//   restore [data]     Restaura de um backup (ex: 2026-04-17)
//   migrate            Aplica migrations pendentes
//   seed [grupo]       Roda seeds (all|users|system|test_data)
//   status             Mostra estado atual do banco
//   diff               Compara schema atual vs último backup
//   reset              Reseta o banco e reinstala tudo
//   verify             Verifica integridade dos dados essenciais
//   schema-export      Exporta schema granular para schema/00_full..10_grants
//   dashboard          Gera dashboard HTML interativo (tabelas + infra)
//   help               Mostra ajuda
// =============================================================================

const { execSync, spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const ROOT = __dirname;
const CONFIG = JSON.parse(fs.readFileSync(path.join(ROOT, 'db.config.json'), 'utf8'));
const CONTAINER = CONFIG.container;
const DB = CONFIG.database;
const USER = CONFIG.user;
const MIGRATIONS_DIR = path.resolve(ROOT, CONFIG.migrationsDir || 'migrations');
const SEEDS_DIR = path.resolve(ROOT, CONFIG.seedsDir || 'seeds');
const FIXES_DIR = path.resolve(ROOT, CONFIG.fixesDir || 'fixes');

// ---------------------------------------------------------------------------
// Colors (sem dependências externas)
// ---------------------------------------------------------------------------
const c = {
    reset: '\x1b[0m',
    bold: '\x1b[1m',
    red: '\x1b[31m',
    green: '\x1b[32m',
    yellow: '\x1b[33m',
    blue: '\x1b[34m',
    cyan: '\x1b[36m',
    gray: '\x1b[90m'
};

function log(msg) { console.log(msg); }
function info(msg) { log(`${c.cyan}ℹ${c.reset} ${msg}`); }
function ok(msg)   { log(`${c.green}✔${c.reset} ${msg}`); }
function warn(msg) { log(`${c.yellow}⚠${c.reset} ${msg}`); }
function err(msg)  { log(`${c.red}✖${c.reset} ${msg}`); }
function title(msg){ log(`\n${c.bold}${c.blue}═══ ${msg} ═══${c.reset}\n`); }
function step(msg) { log(`${c.gray}  →${c.reset} ${msg}`); }

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function dockerRunning() {
    try {
        const result = spawnSync('docker', ['inspect', '-f', '{{.State.Running}}', CONTAINER], {
            encoding: 'utf8',
            timeout: 10000,
            stdio: ['pipe', 'pipe', 'pipe']
        });
        return result.stdout.trim() === 'true';
    } catch {
        return false;
    }
}

function psql(sql, opts = {}) {
    const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} ${opts.tuples ? '-t' : ''} ${opts.quiet ? '-q' : ''} -c "${sql.replace(/"/g, '\\"')}"`;
    return execSync(cmd, {
        encoding: 'utf8',
        timeout: 30000,
        stdio: ['pipe', 'pipe', 'pipe'],
        env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' }
    }).trim();
}

function psqlFile(filePath) {
    const absPath = path.resolve(filePath);
    const content = fs.readFileSync(absPath, 'utf8');
    const utf8Content = "SET client_encoding TO 'UTF8';\n" + content;
    const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} -q -v ON_ERROR_STOP=1`;
    return execSync(cmd, {
        input: utf8Content,
        encoding: 'utf8',
        timeout: 120000,
        stdio: ['pipe', 'pipe', 'pipe'],
        env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' }
    });
}

function pgDump(args) {
    const cmd = `docker exec -e PGCLIENTENCODING=UTF8 ${CONTAINER} pg_dump -U ${USER} -d ${DB} ${args}`;
    return execSync(cmd, { encoding: 'utf8', timeout: 180000, maxBuffer: 100 * 1024 * 1024 });
}

function today() {
    return new Date().toISOString().slice(0, 10);
}

function fileHash(filePath) {
    const content = fs.readFileSync(filePath, 'utf8');
    return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
}

function ensureDir(dir) {
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

function requireDocker() {
    if (!dockerRunning()) {
        err(`Container "${CONTAINER}" não está rodando.`);
        log(`\n  Inicie o Supabase primeiro:`);
        log(`  ${c.cyan}npx supabase start${c.reset}\n`);
        process.exit(1);
    }
}

function listBackups() {
    const dir = path.join(ROOT, 'backups');
    if (!fs.existsSync(dir)) return [];
    return fs
        .readdirSync(dir)
        .filter((f) => /^\d{4}-\d{2}-\d{2}$/.test(f))
        .sort()
        .reverse();
}

// ---------------------------------------------------------------------------
// Migration tracking table
// ---------------------------------------------------------------------------

function ensureMigrationTable() {
    psql(`
    CREATE TABLE IF NOT EXISTS _db_migrations (
      id SERIAL PRIMARY KEY,
      filename TEXT NOT NULL UNIQUE,
      hash TEXT NOT NULL,
      category TEXT NOT NULL DEFAULT 'migration',
      applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
    );
  `);
}

function getAppliedMigrations() {
    ensureMigrationTable();
    const result = psql('SELECT filename, hash, category, applied_at::text FROM _db_migrations ORDER BY id;', { tuples: true });
    if (!result) return [];
    return result
        .split('\n')
        .filter(Boolean)
        .map((line) => {
            const [filename, hash, category, applied_at] = line.split('|').map((s) => s.trim());
            return { filename, hash, category, applied_at };
        });
}

function recordMigration(filename, hash, category) {
    psql(`INSERT INTO _db_migrations (filename, hash, category) VALUES ('${filename}', '${hash}', '${category}') ON CONFLICT (filename) DO UPDATE SET hash = EXCLUDED.hash, applied_at = now();`);
}

// ---------------------------------------------------------------------------
// Supabase-compatible dump builder
// ---------------------------------------------------------------------------

function buildSupabaseDump(backupDir) {
    step('Gerando dump compatível com Supabase (supabase_restore.sql)...');

    const parts = [];

    parts.push('-- ==========================================================');
    parts.push('-- AgenciaPsi — Supabase-compatible full restore dump');
    parts.push(`-- Gerado em: ${new Date().toISOString()}`);
    parts.push('-- ');
    parts.push('-- USO: Para restaurar este dump em um Supabase limpo:');
    parts.push('--   1. npx supabase db reset  (limpa tudo)');
    parts.push('--   2. Rode este arquivo:');
    parts.push(`--      docker exec -i ${CONTAINER} psql -U ${USER} -d ${DB} \\`);
    parts.push('--        < backups/YYYY-MM-DD/supabase_restore.sql');
    parts.push('--');
    parts.push('--   OU via pipe:');
    parts.push('--      cat backups/YYYY-MM-DD/supabase_restore.sql | \\');
    parts.push(`--        docker exec -i ${CONTAINER} psql -U ${USER} -d ${DB}`);
    parts.push('-- ==========================================================');
    parts.push('');
    parts.push("SET client_encoding TO 'UTF8';");
    parts.push('SET statement_timeout = 0;');
    parts.push('SET lock_timeout = 0;');
    parts.push("SET standard_conforming_strings = on;");
    parts.push('');

    // 1. Schema public: drop + recreate
    parts.push('-- [1/5] Limpar schema public');
    parts.push('DROP SCHEMA IF EXISTS public CASCADE;');
    parts.push('CREATE SCHEMA public;');
    parts.push('GRANT ALL ON SCHEMA public TO postgres;');
    parts.push('GRANT ALL ON SCHEMA public TO public;');
    parts.push('');

    // 2. Schema-only dump (public schema only, no infra)
    parts.push('-- [2/5] Estrutura (tabelas, índices, constraints, triggers, functions)');
    const schemaDump = pgDump('--schema-only --no-owner --no-privileges --schema=public');
    parts.push(schemaDump);
    parts.push('');

    // 3. Auth users data (crucial for Supabase)
    parts.push('-- [3/5] Dados auth.users (essencial para autenticação)');
    try {
        const authData = pgDump('--data-only --no-owner --no-privileges --table=auth.users --table=auth.identities --table=auth.sessions --table=auth.refresh_tokens --table=auth.mfa_factors --table=auth.mfa_challenges');
        parts.push(authData);
    } catch {
        parts.push('-- AVISO: Não foi possível exportar dados de auth (pode estar vazio)');
    }
    parts.push('');

    // 4. All public data
    parts.push('-- [4/5] Dados das tabelas public');
    const infraSchemas = ['storage', 'realtime', '_realtime', 'supabase_functions', 'extensions',
        'graphql', 'graphql_public', 'pgsodium', 'vault', 'net', '_analytics',
        'supabase_migrations', 'auth'];
    const excludeFlags = infraSchemas.map((s) => `--exclude-schema=${s}`).join(' ');
    const publicData = pgDump(`--data-only --no-owner --no-privileges ${excludeFlags}`);
    parts.push(publicData);
    parts.push('');

    // 5. Storage buckets/objects metadata (if any)
    parts.push('-- [5/5] Storage buckets (metadados)');
    try {
        const storageBuckets = pgDump('--data-only --no-owner --no-privileges --table=storage.buckets');
        parts.push(storageBuckets);
    } catch {
        parts.push('-- AVISO: Nenhum bucket de storage encontrado');
    }
    parts.push('');

    parts.push('-- Restore finalizado.');

    const dumpContent = parts.join('\n');
    const dumpPath = path.join(backupDir, 'supabase_restore.sql');
    fs.writeFileSync(dumpPath, dumpContent);

    return dumpPath;
}

// ---------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------

const commands = {};

// ---- SETUP ----
commands.setup = function () {
    title('Setup — Instalação do zero');
    requireDocker();

    // 1. Schema base
    const schemaFile = path.join(ROOT, CONFIG.schema);
    if (fs.existsSync(schemaFile)) {
        info('Aplicando schema base...');
        psqlFile(schemaFile);
        ok('Schema aplicado');
    } else {
        warn(`Schema não encontrado: ${schemaFile}`);
        warn('Rode "node db.cjs schema-export" depois de uma migração fresh para gerar o schema.');
    }

    // 2. Fixes (aplicados antes dos seeds para corrigir o schema)
    if (Array.isArray(CONFIG.fixes) && CONFIG.fixes.length > 0) {
        info('Aplicando fixes...');
        let fixCount = 0;
        for (const fix of CONFIG.fixes) {
            const fixPath = path.join(FIXES_DIR, fix);
            if (fs.existsSync(fixPath)) {
                step(fix);
                psqlFile(fixPath);
                fixCount++;
            }
        }
        ok(`${fixCount} fix(es) aplicado(s)`);
    }

    // 3. Seeds (users + system)
    commands.seed('all');

    // 4. Migration tracking table
    ensureMigrationTable();

    // 5. Record seeds + fixes como aplicados
    const allSeeds = [...(CONFIG.seeds?.users || []), ...(CONFIG.seeds?.system || [])];
    for (const seed of allSeeds) {
        const seedPath = path.join(SEEDS_DIR, seed);
        if (fs.existsSync(seedPath)) {
            recordMigration(seed, fileHash(seedPath), 'seed');
        }
    }
    for (const fix of (CONFIG.fixes || [])) {
        const fixPath = path.join(FIXES_DIR, fix);
        if (fs.existsSync(fixPath)) {
            recordMigration(fix, fileHash(fixPath), 'fix');
        }
    }

    // 6. Aplicar migrations incrementais (opcional - só se tiver pendentes)
    if (fs.existsSync(MIGRATIONS_DIR)) {
        const files = fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort();
        if (files.length > 0) {
            info(`Aplicando ${files.length} migration(s)...`);
            for (const file of files) {
                step(file);
                try {
                    psqlFile(path.join(MIGRATIONS_DIR, file));
                    recordMigration(file, fileHash(path.join(MIGRATIONS_DIR, file)), 'migration');
                } catch (e) {
                    warn(`  ${file} falhou (pode já ter sido aplicada via schema/seeds): ${e.message.split('\n')[0]}`);
                    recordMigration(file, fileHash(path.join(MIGRATIONS_DIR, file)), 'migration');
                }
            }
            ok(`${files.length} migration(s) aplicada(s)`);
        }
    }

    ok('Setup completo!');
    log('');

    // 7. Auto-backup
    info('Criando backup pós-setup...');
    commands.backup();

    // 8. Verify
    commands.verify();
};

// ---- BACKUP ----
commands.backup = function () {
    title('Backup');
    requireDocker();

    const date = today();
    const dir = path.join(ROOT, 'backups', date);
    ensureDir(dir);

    const infraSchemas = ['storage', 'realtime', '_realtime', 'supabase_functions', 'extensions',
        'graphql', 'graphql_public', 'pgsodium', 'vault', 'net', '_analytics', 'supabase_migrations'];
    const excludeFlags = infraSchemas.map((s) => `--exclude-schema=${s}`).join(' ');

    step('Exportando schema...');
    const schema = pgDump('--schema-only --no-owner --no-privileges');
    fs.writeFileSync(path.join(dir, 'schema.sql'), schema);

    step('Exportando dados...');
    const data = pgDump(`--data-only --no-owner --no-privileges ${excludeFlags}`);
    fs.writeFileSync(path.join(dir, 'data.sql'), data);

    step('Exportando dump completo...');
    const full = pgDump('--no-owner --no-privileges');
    fs.writeFileSync(path.join(dir, 'full_dump.sql'), full);

    // Dump compatível com Supabase (restauração completa)
    buildSupabaseDump(dir);

    const files = ['schema.sql', 'data.sql', 'full_dump.sql', 'supabase_restore.sql'];
    const sizes = files.map((f) => {
        const fPath = path.join(dir, f);
        if (!fs.existsSync(fPath)) return null;
        const stat = fs.statSync(fPath);
        return `${f}: ${(stat.size / 1024).toFixed(0)}KB`;
    }).filter(Boolean);

    ok(`Backup salvo em backups/${date}/`);
    sizes.forEach((s) => step(s));

    cleanupBackups();
};

function cleanupBackups() {
    const backups = listBackups();
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - CONFIG.backupRetentionDays);
    const cutoffStr = cutoff.toISOString().slice(0, 10);

    let removed = 0;
    for (const b of backups) {
        if (b < cutoffStr) {
            const bDir = path.join(ROOT, 'backups', b);
            fs.rmSync(bDir, { recursive: true, force: true });
            removed++;
        }
    }
    if (removed > 0) {
        info(`${removed} backup(s) antigo(s) removido(s) (>${CONFIG.backupRetentionDays} dias)`);
    }
}

// ---- RESTORE ----
commands.restore = function (dateArg) {
    title('Restore');
    requireDocker();

    const backups = listBackups();
    if (backups.length === 0) {
        err('Nenhum backup encontrado.');
        process.exit(1);
    }

    const date = dateArg || backups[0];
    const dir = path.join(ROOT, 'backups', date);

    if (!fs.existsSync(dir)) {
        err(`Backup não encontrado: ${date}`);
        log(`\n  Backups disponíveis:`);
        backups.forEach((b) => log(`    ${c.cyan}${b}${c.reset}`));
        process.exit(1);
    }

    // Safety backup before restore
    info('Criando backup de segurança antes do restore...');
    try {
        commands.backup();
    } catch {
        warn('Não foi possível criar backup de segurança (banco pode estar vazio)');
    }

    const supaRestore = path.join(dir, 'supabase_restore.sql');
    const fullDump = path.join(dir, 'full_dump.sql');
    const schemaFile = path.join(dir, 'schema.sql');
    const dataFile = path.join(dir, 'data.sql');

    if (fs.existsSync(supaRestore)) {
        info(`Restaurando de backups/${date}/supabase_restore.sql (Supabase-compatible)...`);
        psqlFile(supaRestore);
    } else if (fs.existsSync(fullDump)) {
        info(`Restaurando de backups/${date}/full_dump.sql ...`);
        step('Limpando schema public...');
        psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
        step('Aplicando full dump...');
        psqlFile(fullDump);
    } else if (fs.existsSync(schemaFile) && fs.existsSync(dataFile)) {
        info(`Restaurando de backups/${date}/ (schema + data)...`);
        step('Limpando schema public...');
        psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
        step('Aplicando schema...');
        psqlFile(schemaFile);
        step('Aplicando dados...');
        psqlFile(dataFile);
    } else {
        err(`Backup incompleto em ${date}/`);
        process.exit(1);
    }

    ok(`Banco restaurado de backups/${date}/`);

    commands.verify();
};

// ---- MIGRATE ----
commands.migrate = function () {
    title('Migrate');
    requireDocker();
    ensureMigrationTable();

    if (!fs.existsSync(MIGRATIONS_DIR)) {
        info('Nenhuma pasta migrations/ encontrada.');
        return;
    }

    const files = fs
        .readdirSync(MIGRATIONS_DIR)
        .filter((f) => f.endsWith('.sql'))
        .sort();

    if (files.length === 0) {
        info('Nenhuma migration encontrada.');
        return;
    }

    const applied = getAppliedMigrations().map((m) => m.filename);
    const pending = files.filter((f) => !applied.includes(f));

    if (pending.length === 0) {
        ok('Todas as migrations já foram aplicadas.');
        return;
    }

    // Auto-backup before migrating
    info('Criando backup antes de migrar...');
    commands.backup();

    info(`${pending.length} migration(s) pendente(s):`);
    for (const file of pending) {
        step(`Aplicando ${file}...`);
        const filePath = path.join(MIGRATIONS_DIR, file);
        try {
            psqlFile(filePath);
            recordMigration(file, fileHash(filePath), 'migration');
            ok(`  ${file}`);
        } catch (e) {
            err(`  FALHA em ${file}: ${e.message}`);
            err('Migration abortada. Banco pode estar em estado parcial.');
            err('Use "node db.cjs restore" para voltar ao backup.');
            process.exit(1);
        }
    }

    ok(`${pending.length} migration(s) aplicada(s)`);
};

// ---- SEED ----
commands.seed = function (group) {
    const validGroups = ['all', 'users', 'system', 'test_data'];
    if (!group) group = 'all';

    if (!validGroups.includes(group)) {
        err(`Grupo inválido: ${group}`);
        log(`  Grupos válidos: ${validGroups.join(', ')}`);
        process.exit(1);
    }

    title(`Seeds — ${group}`);
    requireDocker();

    const groups = group === 'all' ? ['users', 'system'] : [group];
    let total = 0;

    for (const g of groups) {
        const seeds = CONFIG.seeds?.[g];
        if (!seeds || seeds.length === 0) continue;

        info(`Grupo: ${g}`);
        for (const seed of seeds) {
            const seedPath = path.join(SEEDS_DIR, seed);
            if (!fs.existsSync(seedPath)) {
                warn(`  Arquivo não encontrado: ${seed}`);
                continue;
            }
            step(seed);
            try {
                psqlFile(seedPath);
                total++;
            } catch (e) {
                err(`  FALHA em ${seed}: ${e.stderr || e.message}`);
                process.exit(1);
            }
        }
    }

    ok(`${total} seed(s) aplicado(s)`);
};

// ---- STATUS ----
commands.status = function () {
    title('Status');
    requireDocker();

    ok(`Container: ${CONTAINER} (rodando)`);

    const backups = listBackups();
    if (backups.length > 0) {
        ok(`Último backup: ${backups[0]}`);
        info(`Total de backups: ${backups.length}`);
    } else {
        warn('Nenhum backup encontrado');
    }

    try {
        const applied = getAppliedMigrations();
        if (applied.length > 0) {
            info(`Registros em _db_migrations: ${applied.length}`);
            applied.slice(-5).forEach((m) => {
                step(`${m.filename} ${c.gray}(${m.category}, ${m.applied_at})${c.reset}`);
            });
        }

        if (fs.existsSync(MIGRATIONS_DIR)) {
            const files = fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql'));
            const pending = files.filter((f) => !applied.map((m) => m.filename).includes(f));
            if (pending.length > 0) {
                warn(`${pending.length} migration(s) pendente(s):`);
                pending.forEach((f) => step(`${c.yellow}${f}${c.reset}`));
            }
        }
    } catch {
        info('Tabela _db_migrations não existe (rode setup primeiro)');
    }

    log('');
    info('Dados no banco:');
    const statusTables = CONFIG.status?.tables || [];
    for (const table of statusTables) {
        try {
            const count = psql(`SELECT count(*) FROM ${table}`, { tuples: true }).trim();
            const color = parseInt(count) > 0 ? c.green : c.red;
            step(`${table}: ${color}${count}${c.reset}`);
        } catch {
            step(`${table}: ${c.gray}(tabela não existe)${c.reset}`);
        }
    }
};

// ---- DIFF ----
commands.diff = function () {
    title('Diff — Schema');
    requireDocker();

    const backups = listBackups();
    if (backups.length === 0) {
        err('Nenhum backup para comparar. Rode "node db.cjs backup" primeiro.');
        process.exit(1);
    }

    const lastBackup = backups[0];
    const lastSchemaPath = path.join(ROOT, 'backups', lastBackup, 'schema.sql');
    if (!fs.existsSync(lastSchemaPath)) {
        err(`Schema não encontrado no backup ${lastBackup}`);
        process.exit(1);
    }

    info('Exportando schema atual...');
    const currentSchema = pgDump('--schema-only --no-owner --no-privileges');
    const lastSchema = fs.readFileSync(lastSchemaPath, 'utf8');

    const extractTables = (sql) => {
        const tables = {};
        const regex = /CREATE TABLE (?:IF NOT EXISTS )?(\S+)\s*\(([\s\S]*?)\);/g;
        let match;
        while ((match = regex.exec(sql)) !== null) {
            tables[match[1]] = match[2].trim();
        }
        return tables;
    };

    const currentTables = extractTables(currentSchema);
    const lastTables = extractTables(lastSchema);
    const allTables = new Set([...Object.keys(currentTables), ...Object.keys(lastTables)]);

    let added = 0, removed = 0, changed = 0, unchanged = 0;

    for (const table of [...allTables].sort()) {
        if (!lastTables[table]) {
            log(`  ${c.green}+ ${table}${c.reset} (nova)`);
            added++;
        } else if (!currentTables[table]) {
            log(`  ${c.red}- ${table}${c.reset} (removida)`);
            removed++;
        } else if (currentTables[table] !== lastTables[table]) {
            log(`  ${c.yellow}~ ${table}${c.reset} (alterada)`);
            changed++;
        } else {
            unchanged++;
        }
    }

    log('');
    ok(`Comparado com backup de ${lastBackup}:`);
    step(`${added} nova(s), ${changed} alterada(s), ${removed} removida(s), ${unchanged} sem mudança`);
};

// ---- RESET ----
commands.reset = function () {
    title('Reset — CUIDADO');
    requireDocker();

    info('Criando backup antes do reset...');
    try {
        commands.backup();
    } catch {
        warn('Não foi possível criar backup');
    }

    warn('Resetando schema public...');
    psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
    ok('Schema public resetado');

    commands.setup();
};

// ---- VERIFY ----
commands.verify = function () {
    title('Verificação de integridade');
    requireDocker();

    let pass = 0, fail = 0;
    const verify = CONFIG.verify || { tables: [], views: [] };

    for (const check of (verify.tables || [])) {
        try {
            const count = parseInt(psql(`SELECT count(*) FROM ${check.name}`, { tuples: true }).trim());
            if (count >= check.min) {
                ok(`${check.name}: ${count} (mín: ${check.min})`);
                pass++;
            } else {
                err(`${check.name}: ${count} (esperado ≥ ${check.min})`);
                fail++;
            }
        } catch {
            err(`${check.name}: tabela não existe`);
            fail++;
        }
    }

    for (const view of (verify.views || [])) {
        try {
            const cnt = psql(`SELECT count(*) FROM ${view};`, { tuples: true }).trim();
            ok(`${view}: ${cnt} registros`);
            pass++;
        } catch {
            err(`${view}: view não existe`);
            fail++;
        }
    }

    log('');
    if (fail === 0) {
        ok(`${c.bold}Todos os ${pass} checks passaram!${c.reset}`);
    } else {
        err(`${fail} check(s) falharam, ${pass} passaram`);
    }
};

// ---- SCHEMA-EXPORT ----
commands['schema-export'] = function () {
    title('Schema Export');
    requireDocker();

    const schemaDir = path.join(ROOT, 'schema');
    const dirs = {
        full:         path.join(schemaDir, '00_full'),
        extensions:   path.join(schemaDir, '01_extensions'),
        types:        path.join(schemaDir, '02_types'),
        functions:    path.join(schemaDir, '03_functions'),
        tables:       path.join(schemaDir, '04_tables'),
        views:        path.join(schemaDir, '05_views'),
        indexes:      path.join(schemaDir, '06_indexes'),
        foreignKeys:  path.join(schemaDir, '07_foreign_keys'),
        triggers:     path.join(schemaDir, '08_triggers'),
        policies:     path.join(schemaDir, '09_policies'),
        grants:       path.join(schemaDir, '10_grants'),
    };
    // Limpa diretórios antes para remover arquivos stale de exports anteriores
    for (const dir of Object.values(dirs)) {
        if (fs.existsSync(dir)) {
            for (const f of fs.readdirSync(dir)) {
                if (f.endsWith('.sql')) fs.rmSync(path.join(dir, f), { force: true });
            }
        }
        ensureDir(dir);
    }

    // 00_full — dump completo
    step('00_full/schema.sql (dump completo)...');
    const fullSchema = pgDump('--schema-only --no-owner --no-privileges');
    fs.writeFileSync(path.join(dirs.full, 'schema.sql'), fullSchema);

    // 01_extensions
    step('01_extensions...');
    const extSql = `SELECT 'CREATE EXTENSION IF NOT EXISTS ' || quote_ident(extname) || ' WITH SCHEMA ' || quote_ident(nspname) || ';' FROM pg_extension e JOIN pg_namespace n ON e.extnamespace = n.oid WHERE extname NOT IN ('plpgsql') ORDER BY extname;`;
    const extResult = psql(extSql, { tuples: true });
    const extLines = extResult.split('\n').filter(Boolean).map(l => l.trim()).filter(Boolean);
    if (extLines.length > 0) {
        const content = `-- Extensions\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${extLines.length}\n\n` + extLines.join('\n') + '\n';
        fs.writeFileSync(path.join(dirs.extensions, 'extensions.sql'), content);
    }

    // 02_types — enums públicos
    step('02_types...');
    const typesSql = `
      SELECT pg_catalog.format_type(t.oid, NULL) || ' AS ENUM (' ||
        string_agg(quote_literal(e.enumlabel), ', ' ORDER BY e.enumsortorder) || ');'
      FROM pg_type t
      JOIN pg_enum e ON t.oid = e.enumtypid
      JOIN pg_namespace n ON t.typnamespace = n.oid
      WHERE n.nspname = 'public'
      GROUP BY t.oid, t.typname;`;
    const typesResult = psql(typesSql, { tuples: true });
    const typesLines = typesResult.split('\n').filter(Boolean).map(l => l.trim()).filter(Boolean);
    if (typesLines.length > 0) {
        const typesContent = `-- Public Enums & Types\n-- Gerado automaticamente em ${new Date().toISOString()}\n\n` +
            typesLines.map(l => `CREATE TYPE ${l}`).join('\n\n') + '\n';
        fs.writeFileSync(path.join(dirs.types, 'public_types.sql'), typesContent);
    }

    try {
        const authTypesSql = typesSql.replace("'public'", "'auth'");
        const authTypesResult = psql(authTypesSql, { tuples: true });
        const authLines = authTypesResult.split('\n').filter(Boolean).map(l => l.trim()).filter(Boolean);
        if (authLines.length > 0) {
            const authContent = `-- Auth Enums & Types\n-- Gerado automaticamente em ${new Date().toISOString()}\n\n` +
                authLines.map(l => `CREATE TYPE ${l}`).join('\n\n') + '\n';
            fs.writeFileSync(path.join(dirs.types, 'auth_types.sql'), authContent);
        }
    } catch { /* auth types may not exist */ }

    // 03_functions
    step('03_functions...');
    const funcBlocks = fullSchema.match(/CREATE(?:\s+OR\s+REPLACE)?\s+FUNCTION\s+[\s\S]*?(?:\$\$[^$]*\$\$|\$[a-zA-Z_]+\$[\s\S]*?\$[a-zA-Z_]+\$)\s*(?:;|$)/gi) || [];
    const funcsBySchema = {};
    for (const block of funcBlocks) {
        const nameMatch = block.match(/FUNCTION\s+([\w.]+)\./);
        const schema = nameMatch ? nameMatch[1] : 'public';
        if (!funcsBySchema[schema]) funcsBySchema[schema] = [];
        funcsBySchema[schema].push(block);
    }
    for (const [schema, funcs] of Object.entries(funcsBySchema)) {
        const content = `-- Functions: ${schema}\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${funcs.length}\n\n` +
            funcs.join('\n\n') + '\n';
        fs.writeFileSync(path.join(dirs.functions, `${schema}.sql`), content);
    }
    if (funcBlocks.length > 0) {
        const allFuncs = `-- All Functions\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${funcBlocks.length}\n\n` +
            funcBlocks.join('\n\n') + '\n';
        fs.writeFileSync(path.join(dirs.functions, '_all.sql'), allFuncs);
    }

    // 04_tables — agrupado por domínio
    step('04_tables...');
    const tableBlocks = fullSchema.match(/CREATE TABLE (?:IF NOT EXISTS )?public\.\S+\s*\([\s\S]*?\);/gi) || [];
    const domainTables = CONFIG.domains || {};
    const tableToDomain = {};
    for (const [domain, tables] of Object.entries(domainTables)) {
        for (const t of tables) tableToDomain[t] = domain;
    }
    const tablesByDomain = {};
    for (const block of tableBlocks) {
        const nameMatch = block.match(/CREATE TABLE (?:IF NOT EXISTS )?public\.(\S+)/i);
        if (!nameMatch) continue;
        const name = nameMatch[1];
        const domain = tableToDomain[name] || 'outros';
        if (!tablesByDomain[domain]) tablesByDomain[domain] = [];
        tablesByDomain[domain].push(block);
    }
    for (const [domain, blocks] of Object.entries(tablesByDomain)) {
        const content = `-- Tables: ${domain}\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${blocks.length}\n\n` +
            blocks.join('\n\n') + '\n';
        const filename = domain.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '') + '.sql';
        fs.writeFileSync(path.join(dirs.tables, filename), content);
    }

    // 05_views
    step('05_views...');
    const viewBlocks = fullSchema.match(/CREATE(?:\s+OR\s+REPLACE)?\s+VIEW\s+public\.[\s\S]*?;/gi) || [];
    if (viewBlocks.length > 0) {
        const content = `-- Views\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${viewBlocks.length}\n\n` +
            viewBlocks.join('\n\n') + '\n';
        fs.writeFileSync(path.join(dirs.views, 'views.sql'), content);
    }

    // 06_indexes
    step('06_indexes...');
    const indexLines = fullSchema.match(/CREATE (?:UNIQUE )?INDEX\s+\S+\s+ON\s+public\.\S+[\s\S]*?;/gi) || [];
    if (indexLines.length > 0) {
        const content = `-- Indexes\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${indexLines.length}\n\n` +
            indexLines.join('\n\n') + '\n';
        fs.writeFileSync(path.join(dirs.indexes, 'indexes.sql'), content);
    }

    // 07_foreign_keys (+ PKs + UNIQUEs)
    step('07_foreign_keys...');
    const constraintLines = fullSchema.match(/ALTER TABLE ONLY public\.\S+\s+ADD CONSTRAINT\s+[\s\S]*?;/gi) || [];
    if (constraintLines.length > 0) {
        const content = `-- Constraints (PK, FK, UNIQUE, CHECK)\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${constraintLines.length}\n\n` +
            constraintLines.join('\n\n') + '\n';
        fs.writeFileSync(path.join(dirs.foreignKeys, 'constraints.sql'), content);
    }

    // 08_triggers
    step('08_triggers...');
    const triggerLines = fullSchema.match(/CREATE TRIGGER\s+[\s\S]*?;/gi) || [];
    if (triggerLines.length > 0) {
        const content = `-- Triggers\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${triggerLines.length}\n\n` +
            triggerLines.join('\n\n') + '\n';
        fs.writeFileSync(path.join(dirs.triggers, 'triggers.sql'), content);
    }

    // 09_policies
    step('09_policies...');
    const policyEnables = fullSchema.match(/ALTER TABLE public\.\S+ ENABLE ROW LEVEL SECURITY;/gi) || [];
    const policyCreates = fullSchema.match(/CREATE POLICY\s+[\s\S]*?;/gi) || [];
    if (policyEnables.length > 0 || policyCreates.length > 0) {
        const content = `-- RLS Policies\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Enable RLS: ${policyEnables.length} tabelas\n-- Policies: ${policyCreates.length}\n\n` +
            '-- Enable RLS\n' + policyEnables.join('\n') + '\n\n' +
            '-- Policies\n' + policyCreates.join('\n\n') + '\n';
        fs.writeFileSync(path.join(dirs.policies, 'policies.sql'), content);
    }

    // 10_grants
    step('10_grants...');
    const grantLines = fullSchema.match(/(?:GRANT|REVOKE)\s+[\s\S]*?;/gi) || [];
    const publicGrants = grantLines.filter(g => /public\./i.test(g));
    if (publicGrants.length > 0) {
        const content = `-- Grants\n-- Gerado automaticamente em ${new Date().toISOString()}\n-- Total: ${publicGrants.length}\n\n` +
            publicGrants.join('\n') + '\n';
        fs.writeFileSync(path.join(dirs.grants, 'grants.sql'), content);
    }

    // Summary
    log('');
    ok('Schema exportado para schema/');
    const summary = [
        ['00_full',         '1 arquivo'],
        ['01_extensions',   fs.readdirSync(dirs.extensions).length + ' arquivo(s)'],
        ['02_types',        fs.readdirSync(dirs.types).length + ' arquivo(s)'],
        ['03_functions',    fs.readdirSync(dirs.functions).length + ' arquivo(s), ' + funcBlocks.length + ' functions'],
        ['04_tables',       fs.readdirSync(dirs.tables).length + ' arquivo(s), ' + tableBlocks.length + ' tabelas'],
        ['05_views',        viewBlocks.length + ' views'],
        ['06_indexes',      indexLines.length + ' indexes'],
        ['07_foreign_keys', constraintLines.length + ' constraints'],
        ['08_triggers',     triggerLines.length + ' triggers'],
        ['09_policies',     policyCreates.length + ' policies'],
        ['10_grants',       publicGrants.length + ' grants'],
    ];
    for (const [dir, desc] of summary) {
        step(`${dir}: ${desc}`);
    }
};

// ---- DASHBOARD ----
commands.dashboard = function (dateArg) {
    title('Dashboard');

    const scriptPath = path.join(ROOT, 'generate-dashboard.cjs');
    if (!fs.existsSync(scriptPath)) {
        err(`Script não encontrado: ${scriptPath}`);
        process.exit(1);
    }

    const args = dateArg ? [scriptPath, dateArg] : [scriptPath];
    const result = spawnSync('node', args, {
        stdio: 'inherit',
        cwd: ROOT,
        env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' }
    });

    if (result.status !== 0) {
        err('Falha ao gerar dashboard.');
        process.exit(result.status || 1);
    }
};

// ---- HELP ----
commands.help = function () {
    log(`
${c.bold}AgenciaPsi — Database CLI${c.reset}

${c.cyan}Uso:${c.reset} node db.cjs <comando> [opções]

${c.cyan}Comandos:${c.reset}

  ${c.bold}setup${c.reset}              Instalação do zero (schema + fixes + seeds + migrations)
                       Cria backup automático após concluir

  ${c.bold}backup${c.reset}             Exporta banco para backups/YYYY-MM-DD/
                       Gera: schema.sql, data.sql, full_dump.sql,
                             ${c.green}supabase_restore.sql${c.reset} (restauração completa)

  ${c.bold}restore [data]${c.reset}     Restaura de um backup
                       Prioriza supabase_restore.sql se existir
                       Sem data = último backup disponível
                       Ex: node db.cjs restore 2026-04-17

  ${c.bold}migrate${c.reset}            Aplica migrations pendentes (pasta migrations/)
                       Backup automático antes de aplicar

  ${c.bold}seed [grupo]${c.reset}       Roda seeds (all, users, system, test_data)
                       Ex: node db.cjs seed system

  ${c.bold}status${c.reset}             Mostra estado do banco, backups, migrations

  ${c.bold}diff${c.reset}               Compara schema atual vs último backup

  ${c.bold}reset${c.reset}              Reseta o banco e reinstala tudo do zero
                       ${c.yellow}⚠ Cria backup antes de resetar${c.reset}

  ${c.bold}verify${c.reset}             Verifica integridade dos dados essenciais
                       (tabelas + views definidas em db.config.json → verify)

  ${c.bold}schema-export${c.reset}      Exporta schema separado em schema/
                       00_full, 01_extensions, 02_types, 03_functions,
                       04_tables (agrupado por domínio), 05_views,
                       06_indexes, 07_foreign_keys, 08_triggers,
                       09_policies, 10_grants

  ${c.bold}dashboard [data]${c.reset}   Gera dashboard HTML interativo do banco
                       Tabelas por domínio + seção Infraestrutura + busca
                       Sem data = usa schema do backup mais recente

  ${c.bold}help${c.reset}               Mostra esta ajuda

${c.cyan}Backup Supabase-compatible:${c.reset}

  O comando ${c.bold}backup${c.reset} gera automaticamente o arquivo
  ${c.green}supabase_restore.sql${c.reset} que contém TUDO necessário
  para restaurar o banco do zero:

    • Schema public (tabelas, índices, triggers, functions)
    • Dados auth.users + identities (autenticação)
    • Todos os dados das tabelas public
    • Metadados de storage buckets

  Para restaurar manualmente:
    ${c.gray}# Opção 1: via CLI${c.reset}
    node db.cjs restore

    ${c.gray}# Opção 2: direto no container${c.reset}
    cat backups/2026-04-17/supabase_restore.sql | \\
      docker exec -i ${CONTAINER} psql -U ${USER} -d ${DB}

${c.cyan}Exemplos:${c.reset}

  ${c.gray}# Primeira vez — instala tudo${c.reset}
  node db.cjs setup

  ${c.gray}# Backup diário${c.reset}
  node db.cjs backup

  ${c.gray}# Perdi o banco — restaurar${c.reset}
  node db.cjs restore

  ${c.gray}# Nova migration${c.reset}
  node db.cjs migrate

  ${c.gray}# Ver o que tem no banco${c.reset}
  node db.cjs status

  ${c.gray}# Atualizar as pastas schema/*${c.reset}
  node db.cjs schema-export

  ${c.gray}# Gerar dashboard HTML${c.reset}
  node db.cjs dashboard
`);
};

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

const [, , cmd, ...args] = process.argv;

if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
    commands.help();
    process.exit(0);
}

if (!commands[cmd]) {
    err(`Comando desconhecido: ${cmd}`);
    log(`  Use ${c.cyan}node db.cjs help${c.reset} para ver os comandos disponíveis.`);
    process.exit(1);
}

try {
    commands[cmd](...args);
} catch (e) {
    err(`Erro: ${e.message}`);
    if (process.env.DEBUG) console.error(e);
    process.exit(1);
}
