<?php
/**
 * db_helper.php
 * Camada de acesso a "banco de dados" em arquivos JSON, com trava de arquivos
 * (flock) para evitar corrupção em leituras/escritas concorrentes.
 *
 * Uso:
 *   $clientes = db_read('clientes');
 *   db_write('clientes', $clientes);
 *   db_update('clientes', function(&$data){ $data[] = [...]; });
 */

define('DB_PATH', __DIR__ . '/../db');

// Define o fuso horário do sistema ANTES de qualquer date()/DateTime, para
// que todos os timestamps (logs, auditoria, financeiro, vencimentos) usem
// o horário local configurado, e não o padrão (geralmente UTC) do servidor.
if (!defined('COBRANEX_TIMEZONE_DEFINIDO')) {
    define('COBRANEX_TIMEZONE_DEFINIDO', true);
    $arquivoConfig = DB_PATH . '/config.json';
    $timezone = 'America/Sao_Paulo';
    if (file_exists($arquivoConfig)) {
        $configBruta = json_decode(file_get_contents($arquivoConfig), true);
        $timezone = $configBruta['empresa']['timezone'] ?? $timezone;
    }
    date_default_timezone_set($timezone);
}

/**
 * Garante que o arquivo JSON existe. Se não existir, cria com um valor padrão.
 */
function db_ensure_file(string $nome, $default = []): string
{
    $file = DB_PATH . '/' . $nome . '.json';
    if (!file_exists($file)) {
        file_put_contents($file, json_encode($default, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }
    return $file;
}

/**
 * Lê um arquivo JSON com trava compartilhada (LOCK_SH).
 */
function db_read(string $nome, $default = [])
{
    $file = db_ensure_file($nome, $default);
    $fp = fopen($file, 'r');
    if (!$fp) {
        throw new RuntimeException("Não foi possível abrir o arquivo $nome.json para leitura.");
    }
    $data = $default;
    if (flock($fp, LOCK_SH)) {
        $conteudo = stream_get_contents($fp);
        flock($fp, LOCK_UN);
        $decoded = json_decode($conteudo, true);
        $data = (json_last_error() === JSON_ERROR_NONE) ? $decoded : $default;
    }
    fclose($fp);
    return $data;
}

/**
 * Escreve (sobrescreve) um arquivo JSON com trava exclusiva (LOCK_EX).
 * Escreve em arquivo temporário e faz rename atômico para evitar corrupção.
 */
function db_write(string $nome, $data): bool
{
    $file = db_ensure_file($nome, []);
    $fp = fopen($file, 'c+');
    if (!$fp) {
        throw new RuntimeException("Não foi possível abrir o arquivo $nome.json para escrita.");
    }

    $ok = false;
    if (flock($fp, LOCK_EX)) {
        $tmp = $file . '.tmp_' . uniqid();
        $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        if ($json !== false) {
            file_put_contents($tmp, $json);
            // Trunca e reescreve o conteúdo dentro do handle já travado
            ftruncate($fp, 0);
            rewind($fp);
            fwrite($fp, $json);
            fflush($fp);
            @unlink($tmp);
            $ok = true;
        }
        flock($fp, LOCK_UN);
    }
    fclose($fp);
    return $ok;
}

/**
 * Lê, aplica uma função de alteração e escreve de volta, tudo dentro
 * de uma única trava exclusiva — evita condição de corrida entre
 * leitura e escrita (read-modify-write atômico).
 */
function db_update(string $nome, callable $callback, $default = [])
{
    $file = db_ensure_file($nome, $default);
    $fp = fopen($file, 'c+');
    if (!$fp) {
        throw new RuntimeException("Não foi possível abrir o arquivo $nome.json.");
    }

    $resultado = null;
    if (flock($fp, LOCK_EX)) {
        rewind($fp);
        $conteudo = stream_get_contents($fp);
        $data = json_decode($conteudo, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            $data = $default;
        }

        $resultado = $callback($data);

        $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, $json);
        fflush($fp);
        flock($fp, LOCK_UN);
    }
    fclose($fp);
    return $resultado;
}

/**
 * Gera um ID único simples (timestamp + random) para novos registros.
 */
/**
 * Pasta onde os backups automáticos/manuais ficam salvos DENTRO da própria
 * hospedagem (não é a mesma coisa que o backup baixado pelo navegador).
 * Protegida por .htaccess — nunca acessível direto pela URL.
 */
define('BACKUPS_PATH', __DIR__ . '/../backups');

/**
 * Gera um .zip com todo o banco (db/*.json) e salva DENTRO da pasta
 * backups/ da própria hospedagem — não depende do navegador estar aberto,
 * então funciona tanto pelo botão do painel quanto por um cron automático.
 * Também apaga backups antigos além do limite de retenção.
 */
function gerar_backup_local(int $manterUltimos = 30): array
{
    if (!is_dir(BACKUPS_PATH)) {
        mkdir(BACKUPS_PATH, 0755, true);
    }

    $nomeArquivo = 'backup_cobranex_' . date('Y-m-d_His') . '.zip';
    $caminhoCompleto = BACKUPS_PATH . '/' . $nomeArquivo;

    $zip = new ZipArchive();
    if ($zip->open($caminhoCompleto, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        return ['ok' => false, 'erro' => 'Não foi possível criar o arquivo de backup (verifique permissão de escrita na pasta backups/).'];
    }
    foreach (glob(DB_PATH . '/*.json') as $arquivo) {
        $zip->addFile($arquivo, basename($arquivo));
    }
    $zip->close();

    // Rotação: mantém só os N mais recentes, apaga o resto.
    $arquivos = glob(BACKUPS_PATH . '/backup_cobranex_*.zip');
    usort($arquivos, fn($a, $b) => filemtime($b) <=> filemtime($a));
    $apagados = [];
    foreach (array_slice($arquivos, $manterUltimos) as $antigo) {
        @unlink($antigo);
        $apagados[] = basename($antigo);
    }

    return [
        'ok' => true,
        'arquivo' => $nomeArquivo,
        'tamanho_kb' => round(filesize($caminhoCompleto) / 1024, 1),
        'apagados' => $apagados,
    ];
}

/**
 * Lista os backups já salvos na pasta backups/, mais recente primeiro.
 */
function listar_backups_locais(): array
{
    if (!is_dir(BACKUPS_PATH)) {
        return [];
    }
    $arquivos = glob(BACKUPS_PATH . '/backup_cobranex_*.zip');
    usort($arquivos, fn($a, $b) => filemtime($b) <=> filemtime($a));

    return array_map(function ($caminho) {
        return [
            'nome' => basename($caminho),
            'tamanho_kb' => round(filesize($caminho) / 1024, 1),
            'criado_em' => date('Y-m-d H:i:s', filemtime($caminho)),
        ];
    }, $arquivos);
}

function gerar_id(string $prefixo = ''): string
{
    return $prefixo . strtoupper(substr(uniqid(bin2hex(random_bytes(3)), true), 0, 12));
}

/**
 * Normaliza uma data/hora de vencimento vinda do formulário (que pode vir
 * como "2026-08-20", "2026-08-20T14:30" do input datetime-local, ou já no
 * formato "2026-08-20 14:30:00") para o formato padrão "Y-m-d H:i:s" usado
 * em todo o sistema. Se não conseguir interpretar, usa o padrão informado.
 */
function normalizar_vencimento($valor, string $padrao = ''): string
{
    if ($padrao === '') {
        $padrao = date('Y-m-d 23:59:00', strtotime('+30 days'));
    }
    if (empty($valor)) {
        return $padrao;
    }
    try {
        return (new DateTime($valor))->format('Y-m-d H:i:s');
    } catch (\Throwable $e) {
        return $padrao;
    }
}

/**
 * Registra uma entrada no log de auditoria.
 */
function auditoria_log(string $usuario, string $acao, string $detalhes = ''): void
{
    db_update('auditoria', function (&$data) use ($usuario, $acao, $detalhes) {
        $data[] = [
            'id' => gerar_id('AUD-'),
            'usuario' => $usuario,
            'acao' => $acao,
            'detalhes' => $detalhes,
            'data' => date('Y-m-d H:i:s'),
            'ip' => $_SERVER['REMOTE_ADDR'] ?? 'cli',
        ];
        // mantém só os últimos 5000 registros para não crescer indefinidamente
        if (count($data) > 5000) {
            $data = array_slice($data, -5000);
        }
    }, []);
}
