<?php
declare(strict_types=1);

/**
 * Banco de dados (SQLite, um arquivo na pasta privada): aliados, envios, modelo de colunas,
 * administradores, configurações e tentativas de login.
 */
final class Banco
{
    private const VERSAO = 4;

    /**
     * Modelo padrão das colunas. Campos: nome, outros nomes aceitos (separados por |),
     * obrigatória, pode ficar em branco, numérica, trava.
     * trava: 2 = coluna-chave (sempre obrigatória e preenchida); 1 = não pode sair do modelo,
     * mas a obrigatoriedade pode mudar; 0 = livre.
     */
    public const MODELO_PADRAO = [
        'sellout' => [
            ['CD_ALIADO', 'COD_ALIADO|CODIGO_ALIADO', 1, 0, 0, 2],
            ['TX_ALIADO', 'ALIADO|ALIANZA|NOMBRE_ALIADO', 1, 0, 0, 0],
            ['PAIS_ALIADO', '', 1, 0, 0, 0],
            ['DISTRITO_ALIADO', 'CIUDAD_ALIADO|CIDADE_ALIADO', 1, 0, 0, 0],
            ['REGION_ALIADO', 'REGIAO_ALIADO|PROVINCIA_ALIADO|DEPARTAMENTO_ALIADO', 1, 0, 0, 0],
            ['TIPO_CLIENTE', 'TIPO_DE_CLIENTE', 1, 0, 0, 0],
            ['RUT_CLIENTE', 'RUT', 1, 0, 0, 0],
            ['NOMBRE_CLIENTE', 'NOME_CLIENTE|CLIENTE', 1, 0, 0, 2],
            ['PAIS_CLIENTE', '', 1, 0, 0, 0],
            ['CIUDAD_CLIENTE', 'CIDADE_CLIENTE|DISTRITO_CLIENTE', 1, 0, 0, 0],
            ['REGION_CLIENTE', 'REGIAO_CLIENTE', 1, 0, 0, 0],
            ['CD_ITEM', 'COD_ITEM|CODIGO_ITEM', 1, 0, 0, 2],
            ['TX_ITEM', 'MODELO|DESCRICAO|DESCRIPCION|PRODUCTO|PRODUTO', 1, 0, 0, 0],
            ['CD_ANO', 'ANO', 1, 0, 1, 2],
            ['CD_MES', 'MES', 1, 0, 1, 2],
            ['TX_MES', 'MES_TEXTO|NOMBRE_MES|MES_NOMBRE', 1, 0, 0, 1],
            ['CANTIDAD', 'QUANTIDADE|QTD', 1, 0, 1, 2],
            ['VALOR UNIT USD', 'VALOR UNITARIO USD|PRECIO UNIT USD', 1, 0, 1, 0],
            ['VALOR TOT USD', 'VALOR TOTAL USD', 1, 0, 1, 2],
            ['UNIDADE', 'UNIDAD', 1, 1, 0, 1],
            ['SEGMENTO', '', 1, 1, 0, 1],
            ['FAMILIA', '', 1, 1, 0, 1],
        ],
        'estoque' => [
            ['CD_ALIADO', 'COD_ALIADO|CODIGO_ALIADO', 1, 0, 0, 2],
            ['CD_ANO', 'ANO', 1, 0, 1, 2],
            ['CD_MES', 'MES', 1, 0, 1, 2],
            ['TX_MES', 'MES_TEXTO|NOMBRE_MES|MES_NOMBRE', 0, 0, 0, 1],
            ['CD_ITEM', 'COD_ITEM|CODIGO_ITEM', 1, 0, 0, 2],
            ['QNT_ESTOQUE', 'QTD_ESTOQUE|QUANTIDADE|CANTIDAD|STOCK', 1, 0, 1, 2],
            ['TX_ITEM', 'MODELO|DESCRICAO|DESCRIPCION|PRODUCTO|PRODUTO', 1, 0, 0, 0],
            ['VALOR UNIT USD', 'VALOR UNITARIO USD|PRECIO UNIT USD', 1, 0, 1, 0],
            ['VALOR TOT USD', 'VALOR TOTAL USD', 1, 0, 1, 2],
            ['UNIDADE', 'UNIDAD', 1, 1, 0, 1],
            ['SEGMENTO', '', 1, 1, 0, 1],
            ['FAMILIA', '', 1, 1, 0, 1],
        ],
    ];

    /** Aliados já conhecidos (dos arquivos de exemplo). */
    private const ALIADOS_INICIAIS = [
        ['1013849', 'BRASTEX DEL CARIBE C.A.', 'Venezuela'],
        ['1014066', 'FULLVISION', 'Argentina'],
        ['1014696', 'LINCE', 'Colômbia'],
        ['1015548', 'MACROQUIL', 'Equador'],
        ['1015817', 'BALANIR S.A.', 'Uruguai'],
        ['1016577', 'ELECTRO INTERIOR S.R.L.', 'Uruguai'],
        ['1017896', 'TELEIMPRESORES S.A.', 'Uruguai'],
        ['1177888', 'MARETEC', 'Argentina'],
    ];

    private static ?PDO $pdo = null;

    public static function pdo(): PDO
    {
        if (self::$pdo !== null) {
            return self::$pdo;
        }
        $arquivo = Config::dirDados() . '/sellout.sqlite';
        $novo = !file_exists($arquivo);
        $pdo = new PDO('sqlite:' . $arquivo, null, null, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        $pdo->exec('PRAGMA busy_timeout = 8000');
        $pdo->exec('PRAGMA foreign_keys = ON');
        if ($novo) {
            @chmod($arquivo, 0640);
        }
        self::$pdo = $pdo;
        self::migra($pdo);
        return $pdo;
    }

    private static function migra(PDO $pdo): void
    {
        $pdo->exec('CREATE TABLE IF NOT EXISTS config (chave TEXT PRIMARY KEY, valor TEXT)');
        $versao = (int) (self::config('versao') ?? 0);
        if ($versao >= self::VERSAO) {
            return;
        }
        $pdo->beginTransaction();
        if ($versao >= 1) {
            if ($versao < 2) {
                // versão 2: a coluna TX_MES entra no modelo (se vier, cada linha precisa ter o mesmo mês do CD_MES)
                foreach (['sellout', 'estoque'] as $tipo) {
                    self::acrescentaAoModelo($pdo, $tipo, ['TX_MES', 'MES_TEXTO|NOMBRE_MES|MES_NOMBRE', 0, 0, 0, 1], 'CD_MES');
                }
            }
            if ($versao < 3) {
                // versão 3: o sellout segue o modelo oficial (aba Instrucciones): mais três colunas e TX_MES obrigatória
                self::acrescentaAoModelo($pdo, 'sellout', ['DISTRITO_ALIADO', 'CIUDAD_ALIADO|CIDADE_ALIADO', 1, 0, 0, 0], 'PAIS_ALIADO');
                self::acrescentaAoModelo($pdo, 'sellout', ['REGION_ALIADO', 'REGIAO_ALIADO|PROVINCIA_ALIADO|DEPARTAMENTO_ALIADO', 1, 0, 0, 0], 'DISTRITO_ALIADO');
                self::acrescentaAoModelo($pdo, 'sellout', ['CIUDAD_CLIENTE', 'CIDADE_CLIENTE|DISTRITO_CLIENTE', 1, 0, 0, 0], 'PAIS_CLIENTE');
                $pdo->prepare('UPDATE modelo_colunas SET obrigatoria = 1 WHERE tipo = ? AND nome = ?')->execute(['sellout', 'TX_MES']);
            }
            if ($versao < 4) {
                // versão 4: o que foi completado com o OK do aliado (o site guarda só o arquivo tratado)
                $cols = array_column($pdo->query('PRAGMA table_info(envios)')->fetchAll(), 'name');
                if (!in_array('completou', $cols, true)) {
                    $pdo->exec("ALTER TABLE envios ADD COLUMN completou TEXT NOT NULL DEFAULT ''");
                }
            }
            self::defineConfig('versao', (string) self::VERSAO);
            $pdo->commit();
            return;
        }
        $pdo->exec('CREATE TABLE IF NOT EXISTS admins (
            id INTEGER PRIMARY KEY, usuario TEXT NOT NULL UNIQUE, senha_hash TEXT NOT NULL,
            criado_em TEXT NOT NULL, ultimo_acesso TEXT)');
        $pdo->exec('CREATE TABLE IF NOT EXISTS aliados (
            codigo TEXT PRIMARY KEY, nome TEXT NOT NULL, pais TEXT NOT NULL DEFAULT \'\',
            ativo INTEGER NOT NULL DEFAULT 1, criado_em TEXT NOT NULL, atualizado_em TEXT NOT NULL)');
        $pdo->exec('CREATE TABLE IF NOT EXISTS envios (
            id INTEGER PRIMARY KEY, criado_em TEXT NOT NULL, aliado_codigo TEXT NOT NULL,
            aliado_nome TEXT NOT NULL, empresa_digitada TEXT NOT NULL, tipo TEXT NOT NULL,
            mes_ref TEXT NOT NULL, meses TEXT NOT NULL, linhas INTEGER NOT NULL,
            total_usd REAL NOT NULL, quantidade REAL NOT NULL, clientes INTEGER NOT NULL, itens INTEGER NOT NULL,
            completados INTEGER NOT NULL, sem_classificacao INTEGER NOT NULL, codigos_fora TEXT NOT NULL,
            nome_original TEXT NOT NULL, arquivo TEXT NOT NULL, arquivo_original TEXT NOT NULL,
            tamanho INTEGER NOT NULL, ip TEXT NOT NULL, substituido INTEGER NOT NULL DEFAULT 0,
            completou TEXT NOT NULL DEFAULT \'\')');
        $pdo->exec('CREATE INDEX IF NOT EXISTS envios_grupo ON envios (aliado_codigo, tipo, mes_ref)');
        $pdo->exec('CREATE INDEX IF NOT EXISTS envios_mes ON envios (mes_ref)');
        $pdo->exec('CREATE TABLE IF NOT EXISTS modelo_colunas (
            tipo TEXT NOT NULL, ordem INTEGER NOT NULL, nome TEXT NOT NULL, aliases TEXT NOT NULL,
            obrigatoria INTEGER NOT NULL, pode_vazio INTEGER NOT NULL, numerica INTEGER NOT NULL,
            trava INTEGER NOT NULL, PRIMARY KEY (tipo, nome))');
        $pdo->exec('CREATE TABLE IF NOT EXISTS tentativas (id INTEGER PRIMARY KEY, chave TEXT NOT NULL, momento INTEGER NOT NULL)');
        $pdo->exec('CREATE INDEX IF NOT EXISTS tentativas_chave ON tentativas (chave, momento)');
        $pdo->exec('CREATE TABLE IF NOT EXISTS recusas (
            id INTEGER PRIMARY KEY, criado_em TEXT NOT NULL, aliado_codigo TEXT NOT NULL, tipo TEXT NOT NULL,
            nome_original TEXT NOT NULL, erros TEXT NOT NULL)');

        $agora = self::agora();
        $ins = $pdo->prepare('INSERT OR IGNORE INTO aliados (codigo, nome, pais, ativo, criado_em, atualizado_em) VALUES (?, ?, ?, 1, ?, ?)');
        foreach (self::ALIADOS_INICIAIS as [$c, $n, $p]) {
            $ins->execute([$c, $n, $p, $agora, $agora]);
        }
        $insM = $pdo->prepare('INSERT OR IGNORE INTO modelo_colunas (tipo, ordem, nome, aliases, obrigatoria, pode_vazio, numerica, trava)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
        foreach (self::MODELO_PADRAO as $tipo => $cols) {
            foreach ($cols as $i => [$nome, $al, $ob, $vz, $num, $trava]) {
                $insM->execute([$tipo, $i + 1, $nome, $al, $ob, $vz, $num, $trava]);
            }
        }
        self::defineConfig('versao', (string) self::VERSAO);
        $pdo->commit();
    }

    /** Põe uma coluna no modelo logo depois de outra, se ainda não estiver lá (usado nas atualizações). */
    private static function acrescentaAoModelo(PDO $pdo, string $tipo, array $col, string $depoisDe): void
    {
        $st = $pdo->prepare('SELECT nome, aliases FROM modelo_colunas WHERE tipo = ? ORDER BY ordem, nome');
        $st->execute([$tipo]);
        $nomes = [];
        foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
            $nomes[] = (string) $r['nome'];
            foreach (array_merge([$r['nome']], explode('|', (string) $r['aliases'])) as $n) {
                if (Texto::normalizaCabecalho((string) $n) === Texto::normalizaCabecalho($col[0])) {
                    return; // já existe (talvez acrescentada à mão)
                }
            }
        }
        $pos = array_search($depoisDe, $nomes, true);
        array_splice($nomes, $pos === false ? count($nomes) : $pos + 1, 0, [$col[0]]);
        [$nome, $al, $ob, $vz, $num, $trava] = $col;
        $pdo->prepare('INSERT INTO modelo_colunas (tipo, ordem, nome, aliases, obrigatoria, pode_vazio, numerica, trava)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)')->execute([$tipo, 0, $nome, $al, $ob, $vz, $num, $trava]);
        $up = $pdo->prepare('UPDATE modelo_colunas SET ordem = ? WHERE tipo = ? AND nome = ?');
        foreach ($nomes as $i => $n) {
            $up->execute([$i + 1, $tipo, $n]);
        }
    }

    public static function agora(): string
    {
        return date('Y-m-d H:i:s');
    }

    public static function config(string $chave): ?string
    {
        $st = (self::$pdo ?? self::pdo())->prepare('SELECT valor FROM config WHERE chave = ?');
        $st->execute([$chave]);
        $v = $st->fetchColumn();
        return $v === false ? null : (string) $v;
    }

    public static function defineConfig(string $chave, string $valor): void
    {
        $st = (self::$pdo ?? self::pdo())->prepare('INSERT INTO config (chave, valor) VALUES (?, ?)
            ON CONFLICT(chave) DO UPDATE SET valor = excluded.valor');
        $st->execute([$chave, $valor]);
    }

    // ---------------------------------------------------------------- modelo de colunas

    /** @return list<array{nome:string, aliases:list<string>, aliases_txt:string, obrigatoria:bool, pode_vazio:bool, numerica:bool, trava:int}> */
    public static function modelo(string $tipo): array
    {
        $st = self::pdo()->prepare('SELECT * FROM modelo_colunas WHERE tipo = ? ORDER BY ordem, nome');
        $st->execute([$tipo]);
        $out = [];
        foreach ($st->fetchAll() as $r) {
            $al = array_values(array_filter(array_map('trim', explode('|', (string) $r['aliases'])), static fn ($x) => $x !== ''));
            $out[] = [
                'nome' => (string) $r['nome'],
                'aliases' => $al,
                'aliases_txt' => implode(', ', $al),
                'obrigatoria' => (bool) $r['obrigatoria'],
                'pode_vazio' => (bool) $r['pode_vazio'],
                'numerica' => (bool) $r['numerica'],
                'trava' => (int) $r['trava'],
            ];
        }
        return $out;
    }

    // ---------------------------------------------------------------- aliados

    public static function aliado(string $codigo): ?array
    {
        $st = self::pdo()->prepare('SELECT * FROM aliados WHERE codigo = ?');
        $st->execute([$codigo]);
        $r = $st->fetch();
        return $r === false ? null : $r;
    }

    /** @return list<array<string,mixed>> */
    public static function aliados(): array
    {
        return self::pdo()->query('SELECT a.*,
                (SELECT COUNT(*) FROM envios e WHERE e.aliado_codigo = a.codigo) AS n_envios,
                (SELECT MAX(criado_em) FROM envios e WHERE e.aliado_codigo = a.codigo) AS ultimo_envio
            FROM aliados a ORDER BY a.ativo DESC, a.nome')->fetchAll();
    }

    /** Cria (novo = true) ou atualiza nome e país. Devolve a mensagem do problema, ou null. */
    public static function salvaAliado(string $codigo, string $nome, string $pais, bool $novo): ?string
    {
        $codigo = Texto::codigoAliado($codigo);
        $nome = Texto::apara(preg_replace('/\s+/u', ' ', $nome) ?? $nome);
        $pais = Texto::apara(preg_replace('/\s+/u', ' ', $pais) ?? $pais);
        if (!preg_match('/^[A-Z0-9._-]{1,20}$/', $codigo)) {
            return 'Código inválido. Use o código de aliado (só números ou letras, até 20 caracteres).';
        }
        if ($nome === '' || mb_strlen($nome) > 120) {
            return 'Informe o nome do aliado (até 120 caracteres).';
        }
        if (mb_strlen($pais) > 60) {
            return 'O país pode ter até 60 caracteres.';
        }
        $existe = self::aliado($codigo);
        $agora = self::agora();
        if ($novo) {
            if ($existe !== null) {
                return 'Já existe um aliado com o código ' . $codigo . ' (' . $existe['nome'] . ').';
            }
            self::pdo()->prepare('INSERT INTO aliados (codigo, nome, pais, ativo, criado_em, atualizado_em) VALUES (?, ?, ?, 1, ?, ?)')
                ->execute([$codigo, $nome, $pais, $agora, $agora]);
            return null;
        }
        if ($existe === null) {
            return 'Aliado ' . $codigo . ' não encontrado.';
        }
        self::pdo()->prepare('UPDATE aliados SET nome = ?, pais = ?, atualizado_em = ? WHERE codigo = ?')
            ->execute([$nome, $pais, $agora, $codigo]);
        return null;
    }

    public static function defineAtivo(string $codigo, bool $ativo): void
    {
        self::pdo()->prepare('UPDATE aliados SET ativo = ?, atualizado_em = ? WHERE codigo = ?')
            ->execute([$ativo ? 1 : 0, self::agora(), $codigo]);
    }

    /** Só exclui aliado sem envios (os que têm envios podem ser desativados). */
    public static function excluiAliado(string $codigo): ?string
    {
        $st = self::pdo()->prepare('SELECT COUNT(*) FROM envios WHERE aliado_codigo = ?');
        $st->execute([$codigo]);
        if ((int) $st->fetchColumn() > 0) {
            return 'Este aliado já tem arquivos recebidos. Para impedir novos envios, desative-o.';
        }
        self::pdo()->prepare('DELETE FROM aliados WHERE codigo = ?')->execute([$codigo]);
        return null;
    }

    // ---------------------------------------------------------------- modelo de colunas (edição)

    /**
     * Salva o modelo editado na área administrativa, respeitando a trava de cada coluna.
     * $enviadas: linhas do formulário (original, ordem, nome, aliases, obrigatoria, pode_vazio, numerica, remover).
     * $nova: coluna nova (nome, aliases, obrigatoria, pode_vazio, numerica), ignorada se o nome vier vazio.
     */
    public static function salvaModelo(string $tipo, array $enviadas, array $nova): ?string
    {
        if (!isset(self::MODELO_PADRAO[$tipo])) {
            return 'Tipo inválido.';
        }
        $atual = [];
        foreach (self::modelo($tipo) as $c) {
            $atual[$c['nome']] = $c;
        }
        $final = [];   // [ordem, posição, nome, aliases, obrigatória, pode vazio, numérica, trava]
        $tratadas = [];
        foreach ($enviadas as $pos => $e) {
            $orig = is_array($e) ? (string) ($e['original'] ?? '') : '';
            if (!isset($atual[$orig]) || isset($tratadas[$orig])) {
                continue;
            }
            $tratadas[$orig] = true;
            $c = $atual[$orig];
            $trava = $c['trava'];
            if ($trava === 0 && !empty($e['remover'])) {
                continue;
            }
            $nome = $trava === 0 ? self::limpaNomeColuna((string) ($e['nome'] ?? '')) : $c['nome'];
            if ($nome === '') {
                return 'O nome da coluna não pode ficar vazio (era ' . $orig . ').';
            }
            if ($trava === 2) {
                [$ob, $vz, $num] = [1, 0, $c['numerica'] ? 1 : 0];
            } else {
                [$ob, $vz, $num] = [empty($e['obrigatoria']) ? 0 : 1, empty($e['pode_vazio']) ? 0 : 1, empty($e['numerica']) ? 0 : 1];
            }
            $final[] = [(int) ($e['ordem'] ?? 0), (int) $pos, $nome, self::limpaAliases((string) ($e['aliases'] ?? '')), $ob, $vz, $num, $trava];
        }
        foreach ($atual as $nome => $c) { // colunas que não vieram no formulário ficam como estão
            if (!isset($tratadas[$nome])) {
                $final[] = [999, 999, $nome, implode('|', $c['aliases']), (int) $c['obrigatoria'], (int) $c['pode_vazio'], (int) $c['numerica'], $c['trava']];
            }
        }
        $nomeNovo = self::limpaNomeColuna((string) ($nova['nome'] ?? ''));
        if ($nomeNovo !== '') {
            $final[] = [PHP_INT_MAX, PHP_INT_MAX, $nomeNovo, self::limpaAliases((string) ($nova['aliases'] ?? '')),
                empty($nova['obrigatoria']) ? 0 : 1, empty($nova['pode_vazio']) ? 0 : 1, empty($nova['numerica']) ? 0 : 1, 0];
        }
        // cada nome (ou outro nome aceito) só pode valer para uma coluna
        $dono = [];
        $nomes = [];
        foreach ($final as [, , $nome, $al]) {
            $kn = Texto::normalizaCabecalho($nome);
            if (isset($nomes[$kn])) {
                return 'Há duas colunas chamadas ' . $nome . '.';
            }
            $nomes[$kn] = true;
            foreach (array_merge([$nome], $al === '' ? [] : explode('|', $al)) as $n) {
                $k = Texto::normalizaCabecalho($n);
                if (isset($dono[$k]) && $dono[$k] !== $nome) {
                    return 'O nome "' . $n . '" está em duas colunas (' . $dono[$k] . ' e ' . $nome . '). Cada nome só pode valer para uma coluna.';
                }
                $dono[$k] = $nome;
            }
        }
        usort($final, static fn ($a, $b) => [$a[0], $a[1]] <=> [$b[0], $b[1]]);
        $pdo = self::pdo();
        $pdo->beginTransaction();
        $pdo->prepare('DELETE FROM modelo_colunas WHERE tipo = ?')->execute([$tipo]);
        $ins = $pdo->prepare('INSERT INTO modelo_colunas (tipo, ordem, nome, aliases, obrigatoria, pode_vazio, numerica, trava)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
        foreach ($final as $i => [, , $nome, $al, $ob, $vz, $num, $trava]) {
            $ins->execute([$tipo, $i + 1, $nome, $al, $ob, $vz, $num, $trava]);
        }
        $pdo->commit();
        return null;
    }

    public static function restauraModelo(string $tipo): void
    {
        if (!isset(self::MODELO_PADRAO[$tipo])) {
            return;
        }
        $pdo = self::pdo();
        $pdo->beginTransaction();
        $pdo->prepare('DELETE FROM modelo_colunas WHERE tipo = ?')->execute([$tipo]);
        $ins = $pdo->prepare('INSERT INTO modelo_colunas (tipo, ordem, nome, aliases, obrigatoria, pode_vazio, numerica, trava)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
        foreach (self::MODELO_PADRAO[$tipo] as $i => [$nome, $al, $ob, $vz, $num, $trava]) {
            $ins->execute([$tipo, $i + 1, $nome, $al, $ob, $vz, $num, $trava]);
        }
        $pdo->commit();
    }

    private static function limpaNomeColuna(string $s): string
    {
        $s = Texto::apara(preg_replace('/\s+/u', ' ', $s) ?? $s);
        return mb_substr(mb_strtoupper($s), 0, 60);
    }

    /** "rut; ruc , nit" -> "RUT|RUC|NIT" */
    private static function limpaAliases(string $s): string
    {
        $out = [];
        foreach (preg_split('/[|,;\r\n]+/', $s) ?: [] as $a) {
            $a = mb_substr(mb_strtoupper(Texto::apara(preg_replace('/\s+/u', ' ', $a) ?? $a)), 0, 60);
            if ($a !== '' && !in_array($a, $out, true)) {
                $out[] = $a;
            }
        }
        return implode('|', array_slice($out, 0, 20));
    }

    // ---------------------------------------------------------------- administradores

    public static function admin(string $usuario): ?array
    {
        $st = self::pdo()->prepare('SELECT * FROM admins WHERE usuario = ?');
        $st->execute([mb_strtolower(Texto::apara($usuario))]);
        $r = $st->fetch();
        return $r === false ? null : $r;
    }

    public static function numAdmins(): int
    {
        return (int) self::pdo()->query('SELECT COUNT(*) FROM admins')->fetchColumn();
    }

    /** Cria o administrador ou troca a senha dele. Devolve true se criou. */
    public static function salvaAdmin(string $usuario, string $senha): bool
    {
        $u = mb_strtolower(Texto::apara($usuario));
        $existia = self::admin($u) !== null;
        self::pdo()->prepare('INSERT INTO admins (usuario, senha_hash, criado_em) VALUES (?, ?, ?)
                ON CONFLICT(usuario) DO UPDATE SET senha_hash = excluded.senha_hash')
            ->execute([$u, password_hash($senha, PASSWORD_DEFAULT), self::agora()]);
        return !$existia;
    }

    public static function registraAcessoAdmin(string $usuario): void
    {
        self::pdo()->prepare('UPDATE admins SET ultimo_acesso = ? WHERE usuario = ?')->execute([self::agora(), $usuario]);
    }
}
