<?php
namespace Modules\Fiscal;

use Gestoreasy\Core\Module\ModuleInterface;
use Gestoreasy\Core\Routing\Router;
use Gestoreasy\Core\Http\Request;
use Gestoreasy\Core\Http\Response;
use Gestoreasy\Core\Tenant\ConnectionManager;
use Gestoreasy\Core\Auth\AuthMiddleware;
use Gestoreasy\Core\Auth\Rbac;
use Gestoreasy\Core\Audit\AuditLogger;
use Gestoreasy\Core\Fiscal\Calculator;
use Gestoreasy\Core\Fiscal\VersionRouter;
use Gestoreasy\Core\Fiscal\NfceService;
use Gestoreasy\Core\Fiscal\CertificateStore;
use Gestoreasy\Core\Fiscal\TaxConfig;
use Gestoreasy\Core\Auth\MfaService;
use Gestoreasy\Core\Tenant\TenantContext;
use Gestoreasy\Core\View\View;
use Modules\Fiscal\Controllers\FiscalViewsController;
use Modules\Fiscal\Controllers\TaxGroupsController;
use Gestoreasy\Core\Fiscal\Sped\SpedFiscalGenerator;
use Gestoreasy\Core\Fiscal\Sped\Period as SpedPeriod;
use Gestoreasy\Core\Fiscal\Logging\FiscalLogger;
use Gestoreasy\Core\Fiscal\CteService;
use Gestoreasy\Core\Fiscal\FiscalNotificationService;

class Module implements ModuleInterface {
    public function register(Router $router): void {
        // ==== Views (UI) ====
        $views = new FiscalViewsController();
        $router->get('/fiscal/nfce/preview', [$views, 'viewNfcePreview'], [AuthMiddleware::requireAuthHtml(), Rbac::requirePermissions(['vendas.read'])]);

        // Preview NFC-e (cálculo fiscal, versão-aware via VersionRouter)
        $previewHandler = function (Request $r) {
            // Aceita itens via query (?items=[...]) ou body JSON { items: [...] }
            $itemsParam = $r->query['items'] ?? ($r->body['items'] ?? null);
            if (is_string($itemsParam)) {
                $decoded = json_decode($itemsParam, true);
                if (is_array($decoded)) { $itemsParam = $decoded; }
            }
            $items = is_array($itemsParam) ? $itemsParam : [];
            if (empty($items)) {
                // Fallback: item de demonstração
                $items = [[ 'descricao' => 'Item de teste', 'quantidade' => 1, 'preco' => 10.00, 'ncm' => '00000000', 'cfop' => '5102', 'csosn' => '102' ]];
            }
            // Normalização: garantir números e padrões amigáveis para formulário
            $num = function ($v) {
                if (is_numeric($v)) return (float)$v;
                if (is_string($v)) {
                    $vv = str_replace(',', '.', $v);
                    return is_numeric($vv) ? (float)$vv : null;
                }
                return null;
            };
            $norm = [];
            foreach ($items as $it) {
                $desc = trim((string)($it['descricao'] ?? $it['description'] ?? ''));
                $q = $num($it['quantidade'] ?? $it['qty'] ?? 1) ?? 1.0;
                $p = $num($it['preco'] ?? $it['price'] ?? 0) ?? 0.0;
                $ncm = preg_replace('/\D/', '', (string)($it['ncm'] ?? ''));
                if ($ncm !== '' && strlen($ncm) !== 8) { $ncm = ''; }
                $cfop = preg_replace('/\D/', '', (string)($it['cfop'] ?? ''));
                if ($cfop !== '' && strlen($cfop) !== 4) { $cfop = ''; }
                $csosn = preg_replace('/\D/', '', (string)($it['csosn'] ?? ''));
                if ($csosn !== '' && strlen($csosn) !== 3) { $csosn = ''; }
                $norm[] = [
                    'descricao' => ($desc !== '' ? $desc : 'Item'),
                    'quantidade' => $q,
                    'preco' => $p,
                    'ncm' => $ncm,
                    'cfop' => $cfop,
                    'csosn' => $csosn,
                ];
            }
            try {
                $preview = VersionRouter::previewNfce($norm);
                return Response::json(['ok' => true, 'preview' => $preview]);
            } catch (\Throwable $e) {
                return Response::json(['error' => $e->getMessage()], 500);
            }
        };
        $router->get('/api/fiscal/nfce/preview', $previewHandler, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);
        // Versão POST para aceitar FormData (items[0][...]) sem exigir JSON direto
        $router->post('/api/fiscal/nfce/preview', $previewHandler, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // Preview NFC-e: validação cruzada (legacy vs 2026)
        $crossHandler = function (Request $r) {
            // Aceitar itens via query/body igual ao preview
            $itemsParam = $r->query['items'] ?? ($r->body['items'] ?? null);
            if (is_string($itemsParam)) {
                $decoded = json_decode($itemsParam, true);
                if (is_array($decoded)) { $itemsParam = $decoded; }
            }
            $items = is_array($itemsParam) ? $itemsParam : [];
            if (empty($items)) {
                $items = [[ 'descricao' => 'Item de teste', 'quantidade' => 1, 'preco' => 10.00 ]];
            }
            // Normalização semelhante ao preview
            $num = function ($v) {
                if (is_numeric($v)) return (float)$v;
                if (is_string($v)) {
                    $vv = str_replace(',', '.', $v);
                    return is_numeric($vv) ? (float)$vv : null;
                }
                return null;
            };
            $norm = [];
            foreach ($items as $it) {
                $desc = trim((string)($it['descricao'] ?? $it['description'] ?? ''));
                $q = $num($it['quantidade'] ?? $it['qty'] ?? 1) ?? 1.0;
                $p = $num($it['preco'] ?? $it['price'] ?? 0) ?? 0.0;
                $norm[] = [ 'descricao' => ($desc !== '' ? $desc : 'Item'), 'quantidade' => $q, 'preco' => $p ];
            }
            try {
                $result = VersionRouter::crossValidatePreview($norm);
                return Response::json(['ok' => true, 'data' => $result]);
            } catch (\Throwable $e) {
                return Response::json(['error' => $e->getMessage()], 500);
            }
        };
        $router->get('/api/fiscal/nfce/preview/cross', $crossHandler, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);
        $router->post('/api/fiscal/nfce/preview/cross', $crossHandler, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // ==== Stubs HTML para demais áreas fiscais ====
        $router->get('/fiscal/sped', function(Request $r){
            return View::renderWithLayout('fiscal/sped', ['title' => 'SPED Fiscal/Contábil']);
        }, [AuthMiddleware::requireAuthHtml()]);
        $router->get('/fiscal/apuracao', function(Request $r){
            return View::renderWithLayout('fiscal/apuracao', ['title' => 'Apuração de impostos']);
        }, [AuthMiddleware::requireAuthHtml()]);
        $router->get('/fiscal/obrigacoes', function(Request $r){
            return View::renderWithLayout('fiscal/obrigacoes', ['title' => 'Obrigações acessórias']);
        }, [AuthMiddleware::requireAuthHtml()]);
        $router->get('/fiscal/documentos', function(Request $r){
            return View::renderWithLayout('fiscal/documentos', ['title' => 'Controle de documentos fiscais']);
        }, [AuthMiddleware::requireAuthHtml()]);

        // Página: Configurações Fiscais (modelo, ambiente e opções por documento)
        $router->get('/fiscal/config', function(Request $r){
            return View::renderWithLayout('fiscal/config', ['title' => 'Configurações Fiscais']);
        }, [AuthMiddleware::requireAuthHtml()]);

        // ==== Documentos Fiscais: listagem e download ====
        $router->get('/api/fiscal/documentos', function(Request $r){
            try {
                $tenantId = \Gestoreasy\Core\Tenant\TenantContext::getTenantId();
                $base = dirname(__DIR__, 2);
                $roots = [
                    [$base . '/storage/sped/' . $tenantId, 'SPED'],
                    [$base . '/public/uploads', 'ANEXO']
                ];
                $tipo = (string)($r->query['tipo'] ?? '');
                $start = (string)($r->query['start'] ?? '');
                $end = (string)($r->query['end'] ?? '');
                $q = (string)($r->query['q'] ?? '');
                $page = max(1, (int)($r->query['page'] ?? 1));
                $perPage = max(1, min(100, (int)($r->query['per_page'] ?? 20)));

                $items = [];
                $extAllow = ['xml','txt','pdf','json','zip'];
                foreach ($roots as [$root, $kind]) {
                    if (!is_dir($root)) continue;
                    $rii = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS));
                    foreach ($rii as $file) {
                        if (!$file->isFile()) continue;
                        $path = $file->getPathname();
                        $rel = str_replace($base, '', $path);
                        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
                        if (!in_array($ext, $extAllow, true)) continue;
                        $mtime = $file->getMTime();
                        $item = [
                            'id' => rtrim(strtr(base64_encode($rel), '+/', '-_'), '='),
                            'tipo' => $kind,
                            'nome' => $file->getBasename(),
                            'data' => date('c', $mtime),
                            'tamanho' => $file->getSize(),
                            'caminho' => $rel,
                            'url' => '/api/fiscal/documentos/download?id=' . urlencode(rtrim(strtr(base64_encode($rel), '+/', '-_'), '='))
                        ];
                        $items[] = $item;
                    }
                }
                // Filtros
                $items = array_values(array_filter($items, function($it) use ($tipo, $start, $end, $q){
                    if ($tipo && strtoupper($it['tipo']) !== strtoupper($tipo)) return false;
                    if ($q) {
                        $hay = strtolower(($it['nome'] ?? '') . ' ' . ($it['caminho'] ?? ''));
                        if (!str_contains($hay, strtolower($q))) return false;
                    }
                    if ($start) {
                        $ts = strtotime($start . ' 00:00:00');
                        $idt = strtotime($it['data'] ?? '') ?: 0;
                        if ($idt < $ts) return false;
                    }
                    if ($end) {
                        $te = strtotime($end . ' 23:59:59');
                        $idt = strtotime($it['data'] ?? '') ?: 0;
                        if ($idt > $te) return false;
                    }
                    return true;
                }));
                // Ordenar por data desc
                usort($items, function($a,$b){ return strcmp(($b['data'] ?? ''), ($a['data'] ?? '')); });
                $total = count($items);
                $pages = (int)ceil($total / $perPage);
                $offset = ($page - 1) * $perPage;
                $slice = array_slice($items, $offset, $perPage);
                return Response::json(['ok' => true, 'data' => $slice, 'pagination' => [ 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'pages' => $pages ]]);
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha ao listar documentos', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuthOrApiKey(), Rbac::requirePermissions(['fiscal.sped.read'])]);

        $router->get('/api/fiscal/documentos/download', function(Request $r){
            try {
                $id = (string)($r->query['id'] ?? '');
                if ($id === '') return Response::json(['error' => 'id requerido'], 422);
                $rel = base64_decode(strtr($id, '-_', '+/'));
                $base = dirname(__DIR__, 2);
                $abs = realpath($base . $rel);
                if (!$abs) return Response::json(['error' => 'Arquivo não encontrado'], 404);
                // Segurança: restringir aos diretórios permitidos
                $allowed = [ realpath($base . '/storage/sped'), realpath($base . '/public/uploads') ];
                $ok = false; foreach ($allowed as $al) { if ($al && str_starts_with($abs, $al)) { $ok = true; break; } }
                if (!$ok) return Response::json(['error' => 'Acesso negado'], 403);
                $mime = 'application/octet-stream';
                $ext = strtolower(pathinfo($abs, PATHINFO_EXTENSION));
                if ($ext === 'xml') $mime = 'application/xml';
                if ($ext === 'pdf') $mime = 'application/pdf';
                if ($ext === 'txt') $mime = 'text/plain';
                if ($ext === 'json') $mime = 'application/json';
                if ($ext === 'zip') $mime = 'application/zip';
                $body = @file_get_contents($abs);
                if ($body === false) return Response::json(['error' => 'Falha ao ler arquivo'], 500);
                return new Response(200, $body, [
                    'Content-Type' => $mime,
                    'Content-Disposition' => 'attachment; filename="' . basename($abs) . '"'
                ]);
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha no download', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuthOrApiKey(), Rbac::requirePermissions(['fiscal.sped.read'])]);

        $router->post('/api/fiscal/documentos/upload', function(Request $r){
            try {
                $tenantId = \Gestoreasy\Core\Tenant\TenantContext::getTenantId();
                $base = dirname(__DIR__, 2);
                $tipo = strtoupper((string)($r->body['tipo'] ?? $r->query['tipo'] ?? 'ANEXO'));
                if (!in_array($tipo, ['SPED','ANEXO'], true)) { $tipo = 'ANEXO'; }
                $destRoot = $tipo === 'SPED' ? ($base . '/storage/sped/' . $tenantId) : ($base . '/public/uploads');
                if (!is_dir($destRoot)) { @mkdir($destRoot, 0777, true); }

                $fname = null; $path = null; $data = null;
                if (!empty($r->files['file']) && is_array($r->files['file'])) {
                    $file = $r->files['file'];
                    $orig = (string)($file['name'] ?? 'upload.bin');
                    $safe = preg_replace('/[^A-Za-z0-9._-]/','_', $orig);
                    $fname = date('Ymd_His') . '_' . $safe;
                    $tmp = (string)($file['tmp_name'] ?? '');
                    if ($tmp && is_file($tmp)) {
                        $path = $destRoot . '/' . $fname;
                        if (!@move_uploaded_file($tmp, $path)) {
                            // fallback para ambientes sem move_uploaded_file
                            $data = @file_get_contents($tmp);
                            if ($data === false) return Response::json(['error' => 'Falha ao ler arquivo temporário'], 500);
                            @file_put_contents($path, $data);
                        }
                    } else {
                        return Response::json(['error' => 'Arquivo inválido'], 422);
                    }
                } else {
                    $filename = (string)($r->body['filename'] ?? '');
                    $content64 = (string)($r->body['content_base64'] ?? '');
                    if ($filename === '' || $content64 === '') return Response::json(['error' => 'Conteúdo ausente'], 422);
                    $safe = preg_replace('/[^A-Za-z0-9._-]/','_', $filename);
                    $fname = date('Ymd_His') . '_' . $safe;
                    $bin = base64_decode($content64, true);
                    if ($bin === false) return Response::json(['error' => 'Base64 inválido'], 422);
                    $path = $destRoot . '/' . $fname;
                    @file_put_contents($path, $bin);
                }

                if (!$path || !is_file($path)) return Response::json(['error' => 'Falha ao salvar'], 500);
                $mtime = @filemtime($path) ?: time();
                $rel = str_replace($base, '', $path);
                $item = [
                    'id' => rtrim(strtr(base64_encode($rel), '+/', '-_'), '='),
                    'tipo' => $tipo,
                    'nome' => basename($path),
                    'data' => date('c', $mtime),
                    'tamanho' => @filesize($path) ?: 0,
                    'caminho' => $rel,
                    'url' => '/api/fiscal/documentos/download?id=' . urlencode(rtrim(strtr(base64_encode($rel), '+/', '-_'), '='))
                ];
                return Response::json(['ok' => true, 'item' => $item]);
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha no upload', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuthOrApiKey(), Rbac::requirePermissions(['fiscal.sped.read'])]);

        // ==== Apuração de Impostos (resumo por período) ====
        $router->post('/api/fiscal/apuracao', function(Request $r){
            $year = (int)($r->body['year'] ?? date('Y'));
            $month = (int)($r->body['month'] ?? date('n'));
            $regime = (string)($r->body['regime'] ?? 'simples');
            $opts = [
                'perfil' => (string)($r->body['perfil'] ?? 'A'),
                'ind_ativ' => (int)($r->body['ind_ativ'] ?? 1),
                'cod_fin' => (int)($r->body['cod_fin'] ?? 0),
                'cod_ver' => (string)($r->body['cod_ver'] ?? '017'),
            ];
            try {
                $pdo = ConnectionManager::forTenant();
                $gen = new SpedFiscalGenerator($pdo);
                $period = new SpedPeriod($year, $month);
                $lines = $gen->generate($period, $opts);
                // Parse SPED lines to compute apuração
                $icms_debitos = 0.0; // saídas
                $icms_creditos = 0.0; // entradas
                $icms_st = 0.0;
                $ipi_total = 0.0;
                $pis_total = 0.0;
                $cofins_total = 0.0;
                $bases = [ 'icms' => 0.0, 'icms_in' => 0.0, 'icms_out' => 0.0, 'ipi' => 0.0, 'pis' => 0.0, 'cofins' => 0.0 ];
                $inDocOut = false; $inDocIn = false;
                foreach ($lines as $l) {
                    if (str_starts_with($l, '|C100|')) { $inDocOut = true; $inDocIn = false; continue; }
                    if (str_starts_with($l, '|D100|')) { $inDocIn = true; $inDocOut = false; continue; }
                    // Itens de saídas C170
                    if ($inDocOut && str_starts_with($l, '|C170|')) {
                        $p = explode('|', $l);
                        $vl_bc_icms = (float)($p[12] ?? 0);
                        $vl_icms = (float)($p[14] ?? 0);
                        $vl_bc_pis = (float)($p[24] ?? 0);
                        $vl_pis = (float)($p[26] ?? 0);
                        $vl_bc_cof = (float)($p[28] ?? 0);
                        $vl_cof = (float)($p[30] ?? 0);
                        $vl_bc_ipi = (float)($p[21] ?? 0);
                        $vl_ipi = (float)($p[23] ?? 0);
                        $icms_debitos += $vl_icms; $bases['icms_out'] += $vl_bc_icms; $bases['icms'] += $vl_bc_icms;
                        $pis_total += $vl_pis; $bases['pis'] += $vl_bc_pis;
                        $cofins_total += $vl_cof; $bases['cofins'] += $vl_bc_cof;
                        $ipi_total += $vl_ipi; $bases['ipi'] += $vl_bc_ipi;
                    }
                    // Consolidações de saídas C190 (para ICMS ST)
                    if ($inDocOut && str_starts_with($l, '|C190|')) {
                        $p = explode('|', $l);
                        $vl_icms_st = (float)($p[8] ?? 0);
                        $icms_st += $vl_icms_st;
                    }
                    // Consolidações de entradas D190 (créditos ICMS, PIS/COFINS nas entradas)
                    if ($inDocIn && str_starts_with($l, '|D190|')) {
                        $p = explode('|', $l);
                        $vl_bc_icms = (float)($p[6] ?? 0);
                        $vl_icms = (float)($p[7] ?? 0);
                        $bases['icms_in'] += $vl_bc_icms;
                        $icms_creditos += $vl_icms;
                        // PIS/COFINS em D190: indices 10,11 conforme SpedValidator expectativa
                        $vl_pis = (float)($p[10] ?? 0);
                        $vl_cof = (float)($p[11] ?? 0);
                        $pis_total += $vl_pis; $cofins_total += $vl_cof;
                    }
                }
                // E110: cálculo básico seguindo o validador
                $vl_aj_debitos = 0.0; $vl_aj_creditos = 0.0; $vl_estornos_deb = 0.0; $vl_estornos_cred = 0.0; $vl_sld_credor_ant = 0.0; $vl_tot_ded = 0.0; $deb_esp = 0.0;
                $vl_tot_debitos = round($icms_debitos, 2);
                $vl_tot_creditos = round($icms_creditos, 2);
                $vl_sld_apurado = round($vl_sld_credor_ant + $vl_tot_debitos - $vl_tot_creditos - $vl_aj_creditos + $vl_aj_debitos - $vl_tot_ded, 2);
                $vl_icms_recolher = $vl_sld_apurado >= 0 ? $vl_sld_apurado : 0.0;
                $vl_sld_credor_transportar = $vl_sld_apurado < 0 ? abs($vl_sld_apurado) : 0.0;
                FiscalLogger::read('apuracao', ['year' => $year, 'month' => $month, 'regime' => $regime, 'icms_debitos' => $vl_tot_debitos, 'icms_creditos' => $vl_tot_creditos]);
                return Response::json([
                    'ok' => true,
                    'period' => [ 'year' => $year, 'month' => $month ],
                    'regime' => $regime,
                    'totals' => [
                        'icms' => [ 'debitos' => $vl_tot_debitos, 'creditos' => $vl_tot_creditos, 'saldo_apurado' => $vl_sld_apurado, 'recolher' => $vl_icms_recolher, 'credor_transportar' => $vl_sld_credor_transportar, 'st' => round($icms_st,2), 'bases' => $bases ],
                        'pis' => round($pis_total, 2),
                        'cofins' => round($cofins_total, 2),
                        'ipi' => round($ipi_total, 2)
                    ]
                ]);
            } catch (\Throwable $e) {
                FiscalLogger::error('apuracao', 'failed', ['message' => $e->getMessage(), 'year' => $year, 'month' => $month]);
                return Response::json(['ok' => false, 'error' => 'Falha na apuração', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuthOrApiKey(), Rbac::requirePermissions(['fiscal.sped.read'])]);

        // ==== SPED Fiscal: geração/validação ====
        $router->post('/api/fiscal/sped/generate', function(Request $r){
            $year = (int)($r->body['year'] ?? date('Y'));
            $month = (int)($r->body['month'] ?? date('n'));
            $opts = [
                'perfil' => (string)($r->body['perfil'] ?? 'A'),
                'ind_ativ' => (int)($r->body['ind_ativ'] ?? 1),
                'cod_fin' => (int)($r->body['cod_fin'] ?? 0),
                'cod_ver' => (string)($r->body['cod_ver'] ?? '017'),
                // Parâmetros fiscais opcionais
                'icms_rate' => isset($r->body['icms_rate']) ? (float)$r->body['icms_rate'] : null,
                'pis_rate' => isset($r->body['pis_rate']) ? (float)$r->body['pis_rate'] : null,
                'cofins_rate' => isset($r->body['cofins_rate']) ? (float)$r->body['cofins_rate'] : null,
                'ipi_rate' => isset($r->body['ipi_rate']) ? (float)$r->body['ipi_rate'] : null,
                'icms_st_rate' => isset($r->body['icms_st_rate']) ? (float)$r->body['icms_st_rate'] : null,
                // Ajustes E111 opcionais
                'e111' => isset($r->body['e111']) && is_array($r->body['e111']) ? $r->body['e111'] : null,
                // Dados opcionais para blocos: D100/D190 e K230/K235
                'd100_docs' => isset($r->body['d100_docs']) && is_array($r->body['d100_docs']) ? $r->body['d100_docs'] : [],
                'k230' => isset($r->body['k230']) && is_array($r->body['k230']) ? $r->body['k230'] : [],
                'k235' => isset($r->body['k235']) && is_array($r->body['k235']) ? $r->body['k235'] : [],
            ];
            try {
                $gen = new SpedFiscalGenerator(ConnectionManager::forTenant());
                $period = new SpedPeriod($year, $month);
                $lines = $gen->generate($period, $opts);
                $txt = $gen->toTxt($lines);
                FiscalLogger::write('sped', ['endpoint' => 'generate', 'year' => $year, 'month' => $month, 'opts' => $opts]);
                return Response::json(['ok' => true, 'lines' => $lines, 'txt' => $txt]);
            } catch (\Throwable $e) {
                FiscalLogger::error('sped', 'generate_failed', ['message' => $e->getMessage(), 'year' => $year, 'month' => $month]);
                return Response::json(['ok' => false, 'error' => 'Falha ao gerar SPED', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuthOrApiKey(), Rbac::requirePermissions(['fiscal.sped.read'])]);

        $router->post('/api/fiscal/sped/check', function(Request $r){
            $year = (int)($r->body['year'] ?? date('Y'));
            $month = (int)($r->body['month'] ?? date('n'));
            $opts = [
                'perfil' => (string)($r->body['perfil'] ?? 'A'),
                'ind_ativ' => (int)($r->body['ind_ativ'] ?? 1),
                'cod_fin' => (int)($r->body['cod_fin'] ?? 0),
                'cod_ver' => (string)($r->body['cod_ver'] ?? '017'),
                'icms_rate' => isset($r->body['icms_rate']) ? (float)$r->body['icms_rate'] : null,
                'pis_rate' => isset($r->body['pis_rate']) ? (float)$r->body['pis_rate'] : null,
                'cofins_rate' => isset($r->body['cofins_rate']) ? (float)$r->body['cofins_rate'] : null,
                'ipi_rate' => isset($r->body['ipi_rate']) ? (float)$r->body['ipi_rate'] : null,
                'icms_st_rate' => isset($r->body['icms_st_rate']) ? (float)$r->body['icms_st_rate'] : null,
                'e111' => isset($r->body['e111']) && is_array($r->body['e111']) ? $r->body['e111'] : null,
                'd100_docs' => isset($r->body['d100_docs']) && is_array($r->body['d100_docs']) ? $r->body['d100_docs'] : [],
                'k230' => isset($r->body['k230']) && is_array($r->body['k230']) ? $r->body['k230'] : [],
                'k235' => isset($r->body['k235']) && is_array($r->body['k235']) ? $r->body['k235'] : [],
            ];
            try {
                $gen = new SpedFiscalGenerator(ConnectionManager::forTenant());
                $period = new SpedPeriod($year, $month);
                $lines = $gen->generate($period, $opts);
                $issues = \Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::validateSpedLines($lines);
                FiscalLogger::read('sped', ['endpoint' => 'check', 'year' => $year, 'month' => $month, 'issues_count' => count($issues)]);
                return Response::json(['ok' => empty($issues), 'issues' => $issues]);
            } catch (\Throwable $e) {
                FiscalLogger::error('sped', 'check_failed', ['message' => $e->getMessage(), 'year' => $year, 'month' => $month]);
                return Response::json(['ok' => false, 'error' => 'Falha na checagem SPED', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuthOrApiKey(), Rbac::requirePermissions(['fiscal.sped.read'])]);

        // SPED: validação leve de dados de companhia (CNPJ/UF)
        $router->post('/api/fiscal/sped/validate', function(Request $r){
            $issues = [];
            try {
                $pdo = ConnectionManager::forTenant();
                $stmt = $pdo->query("SELECT id, razao_social, nome, cnpj, estado, ie, im, logradouro, numero, bairro, cidade, cep FROM empresas ORDER BY id LIMIT 1");
                $company = $stmt ? $stmt->fetch() : null;
                if (!$company) {
                    $issues[] = 'Empresa não cadastrada';
                } else {
                    // CNPJ e UF
                    $cnpj = (string)($company['cnpj'] ?? '');
                    if (!\Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::validateCNPJ($cnpj)) {
                        $issues[] = 'CNPJ inválido';
                    }
                    $uf = strtoupper((string)($company['estado'] ?? ''));
                    if (!\Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::validateUF($uf)) {
                        $issues[] = 'UF inválida';
                    }

                    // IE e IM
                    $ie = (string)($company['ie'] ?? '');
                    if (!\Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::notEmpty($ie)) {
                        $issues[] = 'IE não informada';
                    }
                    $im = (string)($company['im'] ?? '');
                    if (!\Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::notEmpty($im)) {
                        $issues[] = 'IM não informada';
                    }

                    // Endereço
                    $logradouro = (string)($company['logradouro'] ?? '');
                    $numero = (string)($company['numero'] ?? '');
                    $bairro = (string)($company['bairro'] ?? '');
                    $cidade = (string)($company['cidade'] ?? '');
                    $cep = (string)($company['cep'] ?? '');
                    if (!\Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::notEmpty($logradouro)) {
                        $issues[] = 'Logradouro não informado';
                    }
                    if (!\Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::notEmpty($numero)) {
                        $issues[] = 'Número não informado';
                    }
                    if (!\Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::notEmpty($bairro)) {
                        $issues[] = 'Bairro não informado';
                    }
                    if (!\Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::notEmpty($cidade)) {
                        $issues[] = 'Cidade não informada';
                    }
                    $cepDigits = \Gestoreasy\Core\Fiscal\Sped\Validation\SpedValidator::digitsOnly($cep);
                    if (strlen($cepDigits) !== 8) {
                        $issues[] = 'CEP inválido';
                    }
                }
                FiscalLogger::read('sped', ['endpoint' => 'validate', 'issues_count' => count($issues)]);
                return Response::json(['ok' => empty($issues), 'issues' => $issues]);
            } catch (\Throwable $e) {
                FiscalLogger::error('sped', 'validate_failed', ['message' => $e->getMessage()]);
                return Response::json(['ok' => false, 'error' => 'Falha na validação', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuthOrApiKey(), Rbac::requirePermissions(['fiscal.sped.read'])]);

        // SPED: automação para gerar e salvar arquivo TXT
        $router->post('/api/fiscal/sped/auto', function(Request $r){
            $year = (int)($r->body['year'] ?? date('Y'));
            $month = (int)($r->body['month'] ?? date('n'));
            $opts = [
                'perfil' => (string)($r->body['perfil'] ?? 'A'),
                'ind_ativ' => (int)($r->body['ind_ativ'] ?? 1),
                'cod_fin' => (int)($r->body['cod_fin'] ?? 0),
                'cod_ver' => (string)($r->body['cod_ver'] ?? '017'),
                'icms_rate' => isset($r->body['icms_rate']) ? (float)$r->body['icms_rate'] : null,
                'pis_rate' => isset($r->body['pis_rate']) ? (float)$r->body['pis_rate'] : null,
                'cofins_rate' => isset($r->body['cofins_rate']) ? (float)$r->body['cofins_rate'] : null,
                'ipi_rate' => isset($r->body['ipi_rate']) ? (float)$r->body['ipi_rate'] : null,
                'icms_st_rate' => isset($r->body['icms_st_rate']) ? (float)$r->body['icms_st_rate'] : null,
                'e111' => isset($r->body['e111']) && is_array($r->body['e111']) ? $r->body['e111'] : null,
                'd100_docs' => isset($r->body['d100_docs']) && is_array($r->body['d100_docs']) ? $r->body['d100_docs'] : [],
                'k230' => isset($r->body['k230']) && is_array($r->body['k230']) ? $r->body['k230'] : [],
                'k235' => isset($r->body['k235']) && is_array($r->body['k235']) ? $r->body['k235'] : [],
            ];
            $base = getenv('SPED_EXPORT_DIR') ?: (__DIR__ . '/../../storage/sped');
            try {
                $gen = new SpedFiscalGenerator(ConnectionManager::forTenant());
                $period = new SpedPeriod($year, $month);
                $lines = $gen->generate($period, $opts);
                $txt = $gen->toTxt($lines);
                $tenant = \Gestoreasy\Core\Tenant\TenantContext::getTenantId();
                $dir = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $tenant . DIRECTORY_SEPARATOR . sprintf('%04d-%02d', $year, $month);
                if (!is_dir($dir)) { @mkdir($dir, 0775, true); }
                $fname = 'sped_' . date('Ymd_His') . '.txt';
                $path = $dir . DIRECTORY_SEPARATOR . $fname;
                file_put_contents($path, $txt);
                FiscalLogger::write('sped', ['endpoint' => 'auto', 'year' => $year, 'month' => $month, 'path' => $path]);
                return Response::json(['ok' => true, 'path' => $path]);
            } catch (\Throwable $e) {
                FiscalLogger::error('sped', 'auto_failed', ['message' => $e->getMessage(), 'year' => $year, 'month' => $month]);
                return Response::json(['ok' => false, 'error' => 'Falha na automação SPED', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.sped.manage'])]);

        // Endpoint consolidado de status fiscal (NFC-e, CT-e)
        $router->get('/api/fiscal/status', function(Request $r){
            $pdo = ConnectionManager::forTenant();
            $status = [];
            try {
                // NFC-e
                $chk = $pdo->query("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'nfce_emissions'");
                $exists = ((int)($chk->fetch()['c'] ?? 0)) > 0;
                if ($exists) {
                    $rows = $pdo->query("SELECT status, COUNT(*) AS c FROM nfce_emissions GROUP BY status")->fetchAll();
                    $last = $pdo->query("SELECT MAX(created_at) AS ts FROM nfce_emissions")->fetch();
                    $status['nfce'] = ['counts' => $rows, 'last_ts' => ($last['ts'] ?? null)];
                } else { $status['nfce'] = ['counts' => [], 'last_ts' => null]; }
            } catch (\Throwable $e) { $status['nfce_error'] = $e->getMessage(); }
            try {
                // CT-e (se existir)
                $chk = $pdo->query("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'cte_emissions'");
                $exists = ((int)($chk->fetch()['c'] ?? 0)) > 0;
                if ($exists) {
                    $rows = $pdo->query("SELECT status, COUNT(*) AS c FROM cte_emissions GROUP BY status")->fetchAll();
                    $last = $pdo->query("SELECT MAX(created_at) AS ts FROM cte_emissions")->fetch();
                    $status['cte'] = ['counts' => $rows, 'last_ts' => ($last['ts'] ?? null)];
                } else { $status['cte'] = ['counts' => [], 'last_ts' => null]; }
            } catch (\Throwable $e) { $status['cte_error'] = $e->getMessage(); }
            FiscalLogger::read('status', ['endpoint' => 'status', 'nfce' => isset($status['nfce']), 'cte' => isset($status['cte'])]);
            return Response::json(['data' => $status]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // Endpoint de métricas fiscais agregadas por dia
        $router->get('/api/fiscal/metrics', function(Request $r){
            $pdo = ConnectionManager::forTenant();
            $from = (string)($r->query['from'] ?? date('Y-m-d', strtotime('-30 days')));
            $to = (string)($r->query['to'] ?? date('Y-m-d'));
            $metrics = [];
            try {
                $chk = $pdo->query("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'nfce_emissions'");
                $exists = ((int)($chk->fetch()['c'] ?? 0)) > 0;
                if ($exists) {
                    $stmt = $pdo->prepare("SELECT DATE(created_at) AS d, status, COUNT(*) AS c FROM nfce_emissions WHERE DATE(created_at) BETWEEN ? AND ? GROUP BY d, status ORDER BY d ASC");
                    $stmt->execute([$from, $to]);
                    $metrics['nfce'] = $stmt->fetchAll();
                } else { $metrics['nfce'] = []; }
            } catch (\Throwable $e) { $metrics['nfce_error'] = $e->getMessage(); }
            try {
                $chk = $pdo->query("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'cte_emissions'");
                $exists = ((int)($chk->fetch()['c'] ?? 0)) > 0;
                if ($exists) {
                    $stmt = $pdo->prepare("SELECT DATE(created_at) AS d, status, COUNT(*) AS c FROM cte_emissions WHERE DATE(created_at) BETWEEN ? AND ? GROUP BY d, status ORDER BY d ASC");
                    $stmt->execute([$from, $to]);
                    $metrics['cte'] = $stmt->fetchAll();
                } else { $metrics['cte'] = []; }
            } catch (\Throwable $e) { $metrics['cte_error'] = $e->getMessage(); }
            FiscalLogger::read('metrics', ['endpoint' => 'metrics', 'from' => $from, 'to' => $to]);
            return Response::json(['data' => $metrics, 'from' => $from, 'to' => $to]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        $router->post('/api/fiscal/sped/validate', function(Request $r){
            // Por ora validações básicas: presença de empresa e CNPJ/UF
            try {
                $pdo = ConnectionManager::forTenant();
                $stmt = $pdo->query('SELECT cnpj, estado FROM empresas ORDER BY id ASC LIMIT 1');
                $row = $stmt->fetch();
                $issues = [];
                if (!$row) { $issues[] = 'Empresa não cadastrada (tabela empresas vazia)'; }
                else {
                    $cnpj = preg_replace('/\D+/', '', (string)($row['cnpj'] ?? ''));
                    $uf = strtoupper((string)($row['estado'] ?? ''));
                    if (!$cnpj || strlen($cnpj) !== 14) $issues[] = 'CNPJ da empresa ausente ou inválido';
                    if (!preg_match('/^[A-Z]{2}$/', $uf)) $issues[] = 'UF da empresa ausente ou inválida';
                }
                return Response::json(['ok' => empty($issues), 'issues' => $issues]);
            } catch (\Throwable $e) {
                return Response::json(['ok' => false, 'error' => 'Falha na validação', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // Listar emissões NFC-e (histórico)
        $router->get('/api/fiscal/nfce/emissions', function (Request $r) {
            try {
                $pdo = ConnectionManager::forTenant();
                $page = max(1, (int)($r->query['page'] ?? 1));
                $per = min(100, max(1, (int)($r->query['per_page'] ?? 20)));
                $offset = ($page - 1) * $per;
                $status = (string)($r->query['status'] ?? '');
                $env = (string)($r->query['environment'] ?? '');
                $q = (string)($r->query['q'] ?? '');

                $where = [];
                $params = [];
                if ($status !== '') { $where[] = 'status = ?'; $params[] = $status; }
                if ($env !== '') { $where[] = 'environment = ?'; $params[] = $env; }
                if ($q !== '') { $where[] = '(chave LIKE ? OR protocolo LIKE ?)'; $params[] = "%$q%"; $params[] = "%$q%"; }
                $whereSql = count($where) ? ('WHERE ' . implode(' AND ', $where)) : '';

                // Verificar existência da tabela para tolerância em ambientes limpos
                try {
                    $chk = $pdo->query("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'nfce_emissions'");
                    $exists = ((int)($chk->fetch()['c'] ?? 0)) > 0;
                    if (!$exists) {
                        return Response::json(['data' => [], 'page' => $page, 'per_page' => $per, 'total' => 0, 'total_pages' => 0]);
                    }
                } catch (\Throwable $e) {
                    // Se falhar a checagem, prosseguir e capturar abaixo
                }

                $stmt = $pdo->prepare("SELECT id, sale_id, status, chave, protocolo, environment, created_at FROM nfce_emissions $whereSql ORDER BY id DESC LIMIT $per OFFSET $offset");
                $stmt->execute($params);
                $rows = $stmt->fetchAll();
                $cntStmt = $pdo->prepare("SELECT COUNT(*) AS c FROM nfce_emissions $whereSql");
                $cntStmt->execute($params);
                $total = (int)($cntStmt->fetch()['c'] ?? 0);
                $totalPages = (int)ceil($total / $per);
                AuditLogger::logRead('nfce_emissions', null, ['endpoint' => 'fiscal.nfce.emissions.list', 'page' => $page, 'per_page' => $per, 'status' => $status, 'environment' => $env, 'q' => $q]);
                return Response::json(['data' => $rows, 'page' => $page, 'per_page' => $per, 'total' => $total, 'total_pages' => $totalPages]);
            } catch (\Throwable $e) {
                return Response::json(['error' => $e->getMessage(), 'data' => []], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // Autorizar NFC-e (mock homologação RJ)
        $router->post('/api/fiscal/nfce/autorizar', function (Request $r) {
            $body = is_array($r->body ?? null) ? $r->body : [];
            $saleId = (string)($body['sale_id'] ?? $body['saleId'] ?? '');

            // Autoriza a NFC-e via serviço mock
            $result = NfceService::authorizeMockRJ($r, $saleId);

            // Persistir emissão (se a tabela existir)
            try {
                $pdo = ConnectionManager::forTenant();
                $stmt = $pdo->prepare('INSERT INTO nfce_emissions (sale_id, status, chave, protocolo, xml, environment, messages) VALUES (?, ?, ?, ?, ?, ?, ?)');
                $messages = json_encode($result['messages'] ?? [], JSON_UNESCAPED_UNICODE);
                $env = (string)($result['environment'] ?? 'homolog');
                $sid = ($saleId !== '') ? (int)$saleId : null;
                $stmt->execute([$sid, (string)($result['status'] ?? 'authorized'), (string)($result['chave'] ?? ''), (string)($result['protocolo'] ?? ''), (string)($result['xml'] ?? ''), $env, $messages]);

                // Se houver sale_id: marcar pedido como faturado quando ainda não faturado
                if ($sid) {
                    try {
                        $upd = $pdo->prepare("UPDATE sales_orders SET status = 'faturado' WHERE id = ? AND status IN ('aberto','confirmado')");
                        $upd->execute([$sid]);
                    } catch (\Throwable $e) { /* ignora ausência de tabela/coluna */ }
                }
            } catch (\Throwable $e) {
                // Ignorar se tabela não existir; ainda assim retorna a autorização
            }

            // Ajustar status HTTP conforme resultado
            $httpStatus = 200;
            if (isset($result['error'])) {
                $httpStatus = 400;
            } elseif (($result['status'] ?? '') === 'rejected') {
                $httpStatus = 422;
            }
            return Response::json($result, $httpStatus);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.create'])]);

        // Cancelar NFC-e com políticas configuráveis
        $router->post('/api/fiscal/nfce/cancelar', function (Request $r) {
            $body = is_array($r->body ?? null) ? $r->body : [];
            $chave = trim((string)($body['chave'] ?? ''));
            $justificativa = trim((string)($body['justificativa'] ?? ''));
            $environment = (string)($body['environment'] ?? '');

            if ($chave === '' || $justificativa === '') {
                return Response::json(['error' => 'chave e justificativa obrigatórias'], 422);
            }

            $pdo = ConnectionManager::forTenant();
            $cfg = \Gestoreasy\Core\Fiscal\TaxConfig::get($pdo);
            $opt = is_array($cfg['options'] ?? null) ? $cfg['options'] : [];
            $policy = is_array(($opt['cancellation']['nfce'] ?? null)) ? $opt['cancellation']['nfce'] : [];
            $minLen = (int)($policy['min_reason_length'] ?? 15);
            $winMin = (int)($policy['window_minutes'] ?? 0);
            $requireCert = (bool)($policy['require_certificate'] ?? false);
            $envTop = (string)($cfg['environment'] ?? 'production');
            $env = $environment !== '' ? $environment : ($envTop === 'homologation' ? 'homolog' : 'prod');

            if (mb_strlen($justificativa) < $minLen) {
                return Response::json(['error' => 'justificativa muito curta', 'min_chars' => $minLen], 422);
            }

            if ($winMin > 0) {
                try {
                    $stmt = $pdo->prepare('SELECT created_at, authorized_at FROM nfce_emissions WHERE chave = ? ORDER BY id DESC LIMIT 1');
                    $stmt->execute([$chave]);
                    $row = $stmt->fetch();
                    $ts = $row['authorized_at'] ?? ($row['created_at'] ?? null);
                    if ($ts) {
                        $issued = strtotime((string)$ts);
                        if ($issued && (time() - $issued) > ($winMin * 60)) {
                            return Response::json(['error' => 'janela de cancelamento expirada', 'window_minutes' => $winMin], 422);
                        }
                    }
                } catch (\Throwable $e) { /* tolerante: falta de coluna */ }
            }

            if ($requireCert) {
                try {
                    $tenant = \Gestoreasy\Core\Tenant\TenantContext::getTenantId();
                    $selected = \Gestoreasy\Core\Fiscal\CertificateStore::selectCertificate($pdo, $tenant, null, $env === 'prod' ? 'production' : 'homologation');
                    if (!$selected) return Response::json(['error' => 'certificado indisponível para ambiente'], 400);
                } catch (\Throwable $e) {
                    return Response::json(['error' => 'falha ao validar certificado'], 500);
                }
            }

            $result = NfceService::cancelMockRJ($chave, $justificativa);
            $result['environment'] = $env;

            // Persistir cancelamento (quando possível)
            try {
                // Atualiza status se existir, senão insere registro mínimo
                $upd = $pdo->prepare("UPDATE nfce_emissions SET status = 'canceled', messages = CONCAT(IFNULL(messages,''), '\n', ?) WHERE chave = ?");
                $msg = json_encode($result['messages'] ?? [], JSON_UNESCAPED_UNICODE);
                $upd->execute([$msg, $chave]);
                if ($upd->rowCount() === 0) {
                    $ins = $pdo->prepare('INSERT INTO nfce_emissions (sale_id, status, chave, protocolo, xml, environment, messages) VALUES (NULL, ?, ?, ?, NULL, ?, ?)');
                    $ins->execute([(string)($result['status'] ?? 'canceled'), $chave, (string)($result['protocolo'] ?? ''), $env, $msg]);
                }
            } catch (\Throwable $e) { /* ignora */ }

            \Gestoreasy\Core\Fiscal\Logging\FiscalLogger::write('nfce', ['endpoint' => 'cancelar', 'chave' => $chave, 'status' => $result['status'] ?? '', 'env' => $env]);
            return Response::json($result);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.create'])]);

        // Status NFC-e (consultar emissão)
        $router->get('/api/fiscal/nfce/status/{chave}', function (Request $r) {
            $chave = $r->params['chave'] ?? '';
            if ($chave === '') {
                return Response::json(['error' => 'chave obrigatória'], 422);
            }

            $pdo = ConnectionManager::forTenant();
            $stmt = $pdo->prepare('SELECT * FROM nfce_emissions WHERE chave = ? LIMIT 1');
            $stmt->execute([$chave]);
            $emission = $stmt->fetch();

            if (!$emission) {
                return Response::json(['error' => 'Emissão não encontrada'], 404);
            }

            return Response::json(['data' => $emission]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // ========= MFA (TOTP) =========
        $router->post('/api/auth/mfa/setup', function (Request $r) {
            try {
                $pdo = ConnectionManager::forTenant();
                $user = \Gestoreasy\Core\Auth\UserContext::get($r);
                $uid = (int)($user['sub'] ?? 0);
                if ($uid <= 0) return Response::json(['error' => 'usuário inválido'], 403);
                $data = MfaService::getOrCreateSecret($pdo, $uid);
                $secretBin = base64_decode($data['secret'] ?? '', true) ?: random_bytes(20);
                $b32 = MfaService::base32Encode($secretBin);
                $tenant = TenantContext::getTenantId();
                $label = rawurlencode("Gestoreasy:{$tenant}:{$uid}");
                $issuer = rawurlencode('Gestoreasy');
                $otpauth = "otpauth://totp/{$label}?secret={$b32}&issuer={$issuer}&period=30&digits=6";
                return Response::json(['data' => ['base32' => $b32, 'otpauth' => $otpauth, 'enabled' => (bool)$data['enabled']]]);
            } catch (\Throwable $e) { return Response::json(['error' => $e->getMessage()], 500); }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.manage'])]);

        $router->post('/api/auth/mfa/enable', function (Request $r) {
            $code = (string)($r->body['code'] ?? '');
            $pdo = ConnectionManager::forTenant();
            $user = \Gestoreasy\Core\Auth\UserContext::get($r);
            $uid = (int)($user['sub'] ?? 0);
            if ($uid <= 0) return Response::json(['error' => 'usuário inválido'], 403);
            if ($code === '') return Response::json(['error' => 'code requerido'], 422);
            if (!MfaService::verifyCode($pdo, $uid, $code)) return Response::json(['error' => 'código inválido'], 400);
            MfaService::enable($pdo, $uid);
            return Response::json(['ok' => true]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.manage'])]);

        $router->post('/api/auth/mfa/verify', function (Request $r) {
            $code = (string)($r->body['code'] ?? '');
            $pdo = ConnectionManager::forTenant();
            $user = \Gestoreasy\Core\Auth\UserContext::get($r);
            $uid = (int)($user['sub'] ?? 0);
            if ($uid <= 0) return Response::json(['error' => 'usuário inválido'], 403);
            if ($code === '') return Response::json(['error' => 'code requerido'], 422);
            if (!MfaService::verifyCode($pdo, $uid, $code)) return Response::json(['error' => 'código inválido'], 400);
            $token = MfaService::issueToken($uid, 300);
            return Response::json(['data' => ['mfa_token' => $token, 'expires_in' => 300]]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.manage'])]);

        // ========= Certificados =========
        $requireMfa = function (Request $r) {
            $user = \Gestoreasy\Core\Auth\UserContext::get($r);
            $uid = (int)($user['sub'] ?? 0);
            $tok = (string)$r->header('x-mfa-token');
            if ($uid <= 0 || $tok === '') return Response::json(['error' => 'MFA requerido'], 403);
            if (!MfaService::validateToken($tok, $uid)) return Response::json(['error' => 'MFA inválido ou expirado'], 403);
            return null;
        };

        $router->get('/api/fiscal/certificates', function (Request $r) use ($requireMfa) {
            $mfa = $requireMfa($r); if ($mfa instanceof Response) return $mfa;
            $pdo = ConnectionManager::forTenant();
            $tenant = TenantContext::getTenantId();
            $list = CertificateStore::list($pdo, $tenant);
            return Response::json(['data' => $list]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.read'])]);

        $router->post('/api/fiscal/certificates/a1', function (Request $r) use ($requireMfa) {
            $mfa = $requireMfa($r); if ($mfa instanceof Response) return $mfa;
            $b = is_array($r->body ?? null) ? $r->body : [];
            $cnpj = (string)($b['cnpj'] ?? '');
            $env = (string)($b['environment'] ?? 'homologation');
            $pfx = (string)($b['pfx_b64'] ?? '');
            $pwd = (string)($b['password'] ?? '');
            if ($cnpj === '' || $pfx === '' || $pwd === '') return Response::json(['error' => 'cnpj, pfx_b64 e password são obrigatórios'], 422);
            try {
                $pdo = ConnectionManager::forTenant();
                $tenant = TenantContext::getTenantId();
                $id = CertificateStore::storeA1($pdo, $tenant, $cnpj, $env, $pfx, $pwd);
                CertificateStore::logAudit($pdo, $tenant, (int)($r->user['id'] ?? 0), $id, 'create', true, (string)($r->server['REMOTE_ADDR'] ?? ''), (string)($r->server['HTTP_USER_AGENT'] ?? ''));
                return Response::json(['ok' => true, 'id' => $id], 201);
            } catch (\Throwable $e) {
                try { $pdo = ConnectionManager::forTenant(); $tenant = TenantContext::getTenantId(); CertificateStore::logAudit($pdo, $tenant, (int)($r->user['id'] ?? 0), null, 'create', false, (string)($r->server['REMOTE_ADDR'] ?? ''), (string)($r->server['HTTP_USER_AGENT'] ?? ''), $e->getMessage()); } catch (\Throwable $e2) {}
                return Response::json(['error' => $e->getMessage()], 400);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.manage'])]);

        // ========= Configuração Fiscal =========
        $router->get('/api/fiscal/config', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $cfg = TaxConfig::get($pdo);
            return Response::json(['data' => $cfg]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.config.read'])]);

        $router->put('/api/fiscal/config', function (Request $r) {
            $b = is_array($r->body ?? null) ? $r->body : [];
            $mv = (string)($b['model_version'] ?? '');
            $eff = (string)($b['effective_from'] ?? '');
            $env = (string)($b['environment'] ?? 'production');
            $options = isset($b['options']) && is_array($b['options']) ? $b['options'] : [];
            if ($mv === '') return Response::json(['error' => 'model_version requerido'], 422);
            // Validação básica de formato AAAA-MM-DD quando informado
            if ($eff !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $eff)) {
                return Response::json(['error' => 'effective_from inválido (AAAA-MM-DD)'], 422);
            }
            // Validar environment
            $allowedEnv = ['production','homologation'];
            if (!in_array($env, $allowedEnv, true)) {
                return Response::json(['error' => 'environment inválido (use production|homologation)'], 422);
            }
            try {
                $pdo = ConnectionManager::forTenant();
                $cfg = TaxConfig::save($pdo, $mv, ($eff !== '' ? $eff : null), $env, $options);
                return Response::json(['ok' => true, 'data' => $cfg]);
            } catch (\Throwable $e) {
                return Response::json(['error' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.config.manage'])]);

        // ==== Grupos Tributários ====
        $taxGroups = new TaxGroupsController();
        $router->get('/api/fiscal/tax-groups', [$taxGroups, 'list'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);
        $router->get('/api/fiscal/tax-groups/{id}', [$taxGroups, 'getById'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);
        $router->post('/api/fiscal/tax-groups', [$taxGroups, 'create'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.manage'])]);
        $router->put('/api/fiscal/tax-groups/{id}', [$taxGroups, 'update'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.manage'])]);
        $router->delete('/api/fiscal/tax-groups/{id}', [$taxGroups, 'delete'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.manage'])]);
        $router->post('/api/fiscal/tax-groups/calcular', [$taxGroups, 'calcularImpostos'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);

        // ==== CEST (Código Especificador da Substituição Tributária) ====
        $cest = new \Modules\Fiscal\Controllers\CestController();
        $router->get('/api/fiscal/cest/buscar', [$cest, 'buscar'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['produtos.read'])]);
        $router->get('/api/fiscal/cest/listar', [$cest, 'listar'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['produtos.read'])]);
        $router->post('/api/fiscal/cest/mapeamento', [$cest, 'adicionarMapeamento'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.manage'])]);

        // View de Grupos Tributários
        $router->get('/fiscal/grupos-tributarios', function(Request $r){
            return View::renderWithLayout('fiscal/tax-groups', ['title' => 'Grupos Tributários']);
        }, [AuthMiddleware::requireAuthHtml(), Rbac::requirePermissions(['fiscal.read'])]);

        $router->post('/api/fiscal/certificates/a3', function (Request $r) use ($requireMfa) {
            $mfa = $requireMfa($r); if ($mfa instanceof Response) return $mfa;
            $b = is_array($r->body ?? null) ? $r->body : [];
            $cnpj = (string)($b['cnpj'] ?? '');
            $env = (string)($b['environment'] ?? 'production');
            $cfg = [
                'module_path' => (string)($b['module_path'] ?? ''),
                'token_label' => (string)($b['token_label'] ?? ''),
                'cert_label' => (string)($b['cert_label'] ?? ''),
                'pin' => (string)($b['pin'] ?? ''),
            ];
            if ($cnpj === '' || $cfg['module_path'] === '' || $cfg['token_label'] === '' || $cfg['cert_label'] === '') {
                return Response::json(['error' => 'cnpj, module_path, token_label e cert_label são obrigatórios'], 422);
            }
            try {
                $pdo = ConnectionManager::forTenant();
                $tenant = TenantContext::getTenantId();
                $id = CertificateStore::storeA3($pdo, $tenant, $cnpj, $env, $cfg);
                CertificateStore::logAudit($pdo, $tenant, (int)($r->user['id'] ?? 0), $id, 'create', true, (string)($r->server['REMOTE_ADDR'] ?? ''), (string)($r->server['HTTP_USER_AGENT'] ?? ''));
                return Response::json(['ok' => true, 'id' => $id], 201);
            } catch (\Throwable $e) {
                try { $pdo = ConnectionManager::forTenant(); $tenant = TenantContext::getTenantId(); CertificateStore::logAudit($pdo, $tenant, (int)($r->user['id'] ?? 0), null, 'create', false, (string)($r->server['REMOTE_ADDR'] ?? ''), (string)($r->server['HTTP_USER_AGENT'] ?? ''), $e->getMessage()); } catch (\Throwable $e2) {}
                return Response::json(['error' => $e->getMessage()], 400);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.manage'])]);

        $router->get('/api/fiscal/certificates/{id}', function (Request $r) use ($requireMfa) {
            $mfa = $requireMfa($r); if ($mfa instanceof Response) return $mfa;
            $params = $r->get('params', []);
            $id = (int)($params[0] ?? 0);
            if ($id <= 0) return Response::json(['error' => 'id requerido'], 422);
            $pdo = ConnectionManager::forTenant();
            $tenant = TenantContext::getTenantId();
            $row = CertificateStore::get($pdo, $tenant, $id);
            if (!$row) return Response::json(['error' => 'não encontrado'], 404);
            // Não retornar payloads criptografados
            unset($row['payload_enc'], $row['payload_iv'], $row['payload_tag'], $row['kdf_salt']);
            return Response::json(['data' => $row]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.read'])]);

        $router->delete('/api/fiscal/certificates/{id}', function (Request $r) use ($requireMfa) {
            $mfa = $requireMfa($r); if ($mfa instanceof Response) return $mfa;
            $params = $r->get('params', []);
            $id = (int)($params[0] ?? 0);
            if ($id <= 0) return Response::json(['error' => 'id requerido'], 422);
            $pdo = ConnectionManager::forTenant();
            $tenant = TenantContext::getTenantId();
            $ok = CertificateStore::delete($pdo, $tenant, $id);
            CertificateStore::logAudit($pdo, $tenant, (int)($r->user['id'] ?? 0), $id, 'delete', $ok, (string)($r->server['REMOTE_ADDR'] ?? ''), (string)($r->server['HTTP_USER_AGENT'] ?? ''));
            if (!$ok) return Response::json(['error' => 'não encontrado'], 404);
            return Response::json(['ok' => true]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.manage'])]);

        $router->post('/api/fiscal/certificates/{id}/backup', function (Request $r) use ($requireMfa) {
            $mfa = $requireMfa($r); if ($mfa instanceof Response) return $mfa;
            $params = $r->get('params', []);
            $id = (int)($params[0] ?? 0);
            if ($id <= 0) return Response::json(['error' => 'id requerido'], 422);
            $pdo = ConnectionManager::forTenant();
            $tenant = TenantContext::getTenantId();
            $row = CertificateStore::get($pdo, $tenant, $id);
            if (!$row) return Response::json(['error' => 'não encontrado'], 404);
            try {
                $date = date('Ymd_His');
                $base = getenv('CERT_BACKUP_DIR') ?: (__DIR__ . '/../../storage/backups');
                $dir = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . $tenant;
                if (!is_dir($dir)) { @mkdir($dir, 0775, true); }
                $meta = $row; unset($meta['payload_enc'], $meta['payload_iv'], $meta['payload_tag'], $meta['kdf_salt']);
                $bundle = [
                    'meta' => $meta,
                    'payload_enc' => base64_encode($row['payload_enc']),
                    'payload_iv' => base64_encode($row['payload_iv']),
                    'payload_tag' => base64_encode($row['payload_tag']),
                    'kdf_salt' => base64_encode($row['kdf_salt']),
                ];
                $path = $dir . DIRECTORY_SEPARATOR . 'cert_' . $id . '_' . $date . '.json';
                file_put_contents($path, json_encode($bundle, JSON_UNESCAPED_UNICODE));
                CertificateStore::logAudit($pdo, $tenant, (int)($r->user['id'] ?? 0), $id, 'backup', true, (string)($r->server['REMOTE_ADDR'] ?? ''), (string)($r->server['HTTP_USER_AGENT'] ?? ''));
                return Response::json(['ok' => true, 'path' => $path]);
            } catch (\Throwable $e) {
                CertificateStore::logAudit($pdo, $tenant, (int)($r->user['id'] ?? 0), $id, 'backup', false, (string)($r->server['REMOTE_ADDR'] ?? ''), (string)($r->server['HTTP_USER_AGENT'] ?? ''), $e->getMessage());
                return Response::json(['error' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.certs.manage'])]);

        // ========= CT-e (Conhecimento de Transporte Eletrônico) =========
        
        // Listar emissões CT-e (histórico)
        $router->get('/api/fiscal/cte/emissions', function (Request $r) {
            try {
                $pdo = ConnectionManager::forTenant();
                $page = max(1, (int)($r->query['page'] ?? 1));
                $per = min(100, max(1, (int)($r->query['per_page'] ?? 20)));
                $offset = ($page - 1) * $per;
                $status = (string)($r->query['status'] ?? '');
                $env = (string)($r->query['environment'] ?? '');
                $q = (string)($r->query['q'] ?? '');

                $where = [];
                $params = [];
                if ($status !== '') { $where[] = 'status = ?'; $params[] = $status; }
                if ($env !== '') { $where[] = 'environment = ?'; $params[] = $env; }
                if ($q !== '') { $where[] = '(chave LIKE ? OR protocolo LIKE ? OR destinatario_nome LIKE ?)'; $params[] = "%$q%"; $params[] = "%$q%"; $params[] = "%$q%"; }
                $whereSql = count($where) ? ('WHERE ' . implode(' AND ', $where)) : '';

                // Verificar existência da tabela para tolerância em ambientes limpos
                try {
                    $chk = $pdo->query("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'cte_emissions'");
                    $exists = ((int)($chk->fetch()['c'] ?? 0)) > 0;
                    if (!$exists) {
                        return Response::json(['data' => [], 'pagination' => ['page' => $page, 'per_page' => $per, 'total' => 0, 'pages' => 0]]);
                    }
                } catch (\Throwable $e) {
                    return Response::json(['data' => [], 'pagination' => ['page' => $page, 'per_page' => $per, 'total' => 0, 'pages' => 0]]);
                }

                $countSql = "SELECT COUNT(*) AS total FROM cte_emissions $whereSql";
                $stmt = $pdo->prepare($countSql);
                $stmt->execute($params);
                $total = (int)($stmt->fetch()['total'] ?? 0);

                $sql = "SELECT id, chave, numero, serie, status, protocolo, remetente_nome, destinatario_nome, vl_total, environment, created_at FROM cte_emissions $whereSql ORDER BY created_at DESC LIMIT $per OFFSET $offset";
                $stmt = $pdo->prepare($sql);
                $stmt->execute($params);
                $rows = $stmt->fetchAll();

                FiscalLogger::read('cte', ['endpoint' => 'list', 'count' => count($rows), 'filters' => compact('status', 'env', 'q')]);
                return Response::json(['data' => $rows, 'pagination' => ['page' => $page, 'per_page' => $per, 'total' => $total, 'pages' => ceil($total / $per)]]);
            } catch (\Throwable $e) {
                FiscalLogger::error('cte', 'list_failed', ['message' => $e->getMessage()]);
                return Response::json(['error' => 'Falha ao listar CT-e', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // Emitir CT-e
        $router->post('/api/fiscal/cte/autorizar', function (Request $r) {
            $body = is_array($r->body ?? null) ? $r->body : [];
            
            // Validar campos obrigatórios
            $required = ['remetente_cnpj', 'remetente_nome', 'destinatario_nome', 'vl_total'];
            foreach ($required as $field) {
                if (empty($body[$field])) {
                    return Response::json(['error' => "Campo obrigatório: $field"], 422);
                }
            }

            try {
                // Autorizar CT-e via serviço mock
                $result = CteService::authorizeMock($r, $body);

                // Persistir emissão (se a tabela existir)
                try {
                    $pdo = ConnectionManager::forTenant();
                    $stmt = $pdo->prepare('INSERT INTO cte_emissions (
                        chave, numero, serie, status, protocolo, xml, environment, messages,
                        remetente_cnpj, remetente_ie, remetente_nome, remetente_endereco,
                        destinatario_cnpj, destinatario_cpf, destinatario_ie, destinatario_nome, destinatario_endereco,
                        expedidor_cnpj, expedidor_nome, recebedor_cnpj, recebedor_nome,
                        vl_total, vl_receber, vl_icms, vl_icms_st,
                        produto_predominante, peso_bruto, peso_liquido, qtd_volumes, modal, tipo_servico
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
                    
                    $messages = json_encode($result['messages'] ?? [], JSON_UNESCAPED_UNICODE);
                    $env = (string)($result['environment'] ?? 'homolog');
                    
                    $stmt->execute([
                        (string)($result['chave'] ?? ''),
                        (int)($body['numero'] ?? 1),
                        (int)($body['serie'] ?? 1),
                        (string)($result['status'] ?? 'pending'),
                        (string)($result['protocolo'] ?? ''),
                        (string)($result['xml'] ?? ''),
                        $env,
                        $messages,
                        preg_replace('/\D/', '', (string)($body['remetente_cnpj'] ?? '')),
                        (string)($body['remetente_ie'] ?? ''),
                        (string)($body['remetente_nome'] ?? ''),
                        (string)($body['remetente_endereco'] ?? ''),
                        preg_replace('/\D/', '', (string)($body['destinatario_cnpj'] ?? '')),
                        preg_replace('/\D/', '', (string)($body['destinatario_cpf'] ?? '')),
                        (string)($body['destinatario_ie'] ?? ''),
                        (string)($body['destinatario_nome'] ?? ''),
                        (string)($body['destinatario_endereco'] ?? ''),
                        preg_replace('/\D/', '', (string)($body['expedidor_cnpj'] ?? '')),
                        (string)($body['expedidor_nome'] ?? ''),
                        preg_replace('/\D/', '', (string)($body['recebedor_cnpj'] ?? '')),
                        (string)($body['recebedor_nome'] ?? ''),
                        (float)($body['vl_total'] ?? 0),
                        (float)($body['vl_receber'] ?? $body['vl_total'] ?? 0),
                        (float)($body['vl_icms'] ?? 0),
                        (float)($body['vl_icms_st'] ?? 0),
                        (string)($body['produto_predominante'] ?? ''),
                        (float)($body['peso_bruto'] ?? 0),
                        (float)($body['peso_liquido'] ?? 0),
                        (int)($body['qtd_volumes'] ?? 0),
                        (string)($body['modal'] ?? '01'),
                        (string)($body['tipo_servico'] ?? '0')
                    ]);
                } catch (\Throwable $e) {
                    // Ignorar se tabela não existir; ainda assim retorna a autorização
                    FiscalLogger::error('cte', 'persist_failed', ['message' => $e->getMessage()]);
                }

                FiscalLogger::write('cte', ['endpoint' => 'authorize', 'chave' => $result['chave'] ?? '', 'status' => $result['status'] ?? '']);
                return Response::json($result);
            } catch (\Throwable $e) {
                FiscalLogger::error('cte', 'authorize_failed', ['message' => $e->getMessage()]);
                return Response::json(['error' => 'Falha na emissão do CT-e', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.create'])]);

        // Cancelar CT-e
        $router->post('/api/fiscal/cte/cancelar', function (Request $r) {
            $body = is_array($r->body ?? null) ? $r->body : [];
            $chave = trim((string)($body['chave'] ?? ''));
            $justificativa = trim((string)($body['justificativa'] ?? ''));
            $environment = (string)($body['environment'] ?? '');

            if ($chave === '' || $justificativa === '') {
                return Response::json(['error' => 'chave e justificativa obrigatórias'], 422);
            }

            try {
                $pdo = ConnectionManager::forTenant();
                $cfg = \Gestoreasy\Core\Fiscal\TaxConfig::get($pdo);
                $opt = is_array($cfg['options'] ?? null) ? $cfg['options'] : [];
                $policy = is_array(($opt['cancellation']['cte'] ?? null)) ? $opt['cancellation']['cte'] : [];
                $minLen = (int)($policy['min_reason_length'] ?? 15);
                $winMin = (int)($policy['window_minutes'] ?? 0);
                $requireCert = (bool)($policy['require_certificate'] ?? true);
                $envTop = (string)($cfg['environment'] ?? 'production');
                $env = $environment !== '' ? $environment : ($envTop === 'homologation' ? 'homolog' : 'prod');

                if (mb_strlen($justificativa) < $minLen) {
                    return Response::json(['error' => 'justificativa muito curta', 'min_chars' => $minLen], 422);
                }

                if ($winMin > 0) {
                    try {
                        $stmt = $pdo->prepare('SELECT created_at, authorized_at FROM cte_emissions WHERE chave = ? ORDER BY id DESC LIMIT 1');
                        $stmt->execute([$chave]);
                        $row = $stmt->fetch();
                        $ts = $row['authorized_at'] ?? ($row['created_at'] ?? null);
                        if ($ts) {
                            $issued = strtotime((string)$ts);
                            if ($issued && (time() - $issued) > ($winMin * 60)) {
                                return Response::json(['error' => 'janela de cancelamento expirada', 'window_minutes' => $winMin], 422);
                            }
                        }
                    } catch (\Throwable $e) { /* tolerante */ }
                }

                if ($requireCert) {
                    try {
                        $tenant = \Gestoreasy\Core\Tenant\TenantContext::getTenantId();
                        $selected = \Gestoreasy\Core\Fiscal\CertificateStore::selectCertificate($pdo, $tenant, null, $env === 'prod' ? 'production' : 'homologation');
                        if (!$selected) return Response::json(['error' => 'certificado indisponível para ambiente'], 400);
                    } catch (\Throwable $e) { 
                        return Response::json(['error' => 'falha ao validar certificado'], 500);
                    }
                }

                $result = CteService::cancelMock($chave, $justificativa);
                $result['environment'] = $env;

                // Persistir cancelamento (quando possível)
                try {
                    // Atualiza status se existir, senão insere registro mínimo
                    $upd = $pdo->prepare("UPDATE cte_emissions SET status = 'canceled', messages = CONCAT(IFNULL(messages,''), '\n', ?) WHERE chave = ?");
                    $msg = json_encode($result['messages'] ?? [], JSON_UNESCAPED_UNICODE);
                    $upd->execute([$msg, $chave]);
                    if ($upd->rowCount() === 0) {
                        $ins = $pdo->prepare('INSERT INTO cte_emissions (chave, status, protocolo, environment, messages, remetente_cnpj, remetente_nome, destinatario_nome, vl_total) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
                        $ins->execute([$chave, (string)($result['status'] ?? 'canceled'), (string)($result['protocolo'] ?? ''), $env, $msg, '00000000000000', 'Remetente', 'Destinatário', 0]);
                    }
                } catch (\Throwable $e) { 
                    FiscalLogger::error('cte', 'cancel_persist_failed', ['message' => $e->getMessage()]);
                }

                FiscalLogger::write('cte', ['endpoint' => 'cancel', 'chave' => $chave, 'status' => $result['status'] ?? '', 'env' => $env]);
                return Response::json($result);
            } catch (\Throwable $e) {
                FiscalLogger::error('cte', 'cancel_failed', ['message' => $e->getMessage(), 'chave' => $chave]);
                return Response::json(['error' => 'Falha no cancelamento do CT-e', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.create'])]);

        // Status CT-e (consultar emissão)
        $router->get('/api/fiscal/cte/status/{chave}', function (Request $r) {
            $chave = $r->params['chave'] ?? '';
            if ($chave === '') {
                return Response::json(['error' => 'chave obrigatória'], 422);
            }

            try {
                $pdo = ConnectionManager::forTenant();
                $stmt = $pdo->prepare('SELECT * FROM cte_emissions WHERE chave = ? LIMIT 1');
                $stmt->execute([$chave]);
                $emission = $stmt->fetch();

                if (!$emission) {
                    return Response::json(['error' => 'Emissão não encontrada'], 404);
                }

                FiscalLogger::read('cte', ['endpoint' => 'status', 'chave' => $chave]);
                return Response::json(['data' => $emission]);
            } catch (\Throwable $e) {
                FiscalLogger::error('cte', 'status_failed', ['message' => $e->getMessage(), 'chave' => $chave]);
                return Response::json(['error' => 'Falha na consulta do CT-e', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // Consultar CT-e na SEFAZ (mock)
        $router->post('/api/fiscal/cte/consultar', function (Request $r) {
            $body = is_array($r->body ?? null) ? $r->body : [];
            $chave = trim((string)($body['chave'] ?? ''));

            if ($chave === '') {
                return Response::json(['error' => 'chave obrigatória'], 422);
            }

            try {
                $result = CteService::consultMock($chave);
                
                // Atualizar status local se encontrado
                try {
                    $pdo = ConnectionManager::forTenant();
                    $upd = $pdo->prepare("UPDATE cte_emissions SET status = ?, messages = CONCAT(IFNULL(messages,''), '\n', ?) WHERE chave = ?");
                    $msg = json_encode($result['messages'] ?? [], JSON_UNESCAPED_UNICODE);
                    $upd->execute([(string)($result['status'] ?? 'pending'), $msg, $chave]);
                } catch (\Throwable $e) {
                    FiscalLogger::error('cte', 'consult_update_failed', ['message' => $e->getMessage()]);
                }

                FiscalLogger::read('cte', ['endpoint' => 'consult', 'chave' => $chave, 'status' => $result['status'] ?? '']);
                return Response::json($result);
            } catch (\Throwable $e) {
                FiscalLogger::error('cte', 'consult_failed', ['message' => $e->getMessage(), 'chave' => $chave]);
                return Response::json(['error' => 'Falha na consulta SEFAZ', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['vendas.read'])]);

        // ==== ENTRADA DE NOTAS FISCAIS ====
        $nfeEntrada = new \Modules\Fiscal\Controllers\NFeEntradaController();
        
        // Views
        $router->get('/fiscal/entrada-notas', function(Request $r){
            return View::renderWithLayout('fiscal/entrada-notas', ['title' => 'Entrada de Notas Fiscais']);
        }, [AuthMiddleware::requireAuthHtml(), Rbac::requirePermissions(['fiscal.read'])]);
        
        // API - Listagem
        $router->get('/api/fiscal/nfe-entrada', [$nfeEntrada, 'list'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);
        $router->get('/api/fiscal/nfe-entrada/{id}', [$nfeEntrada, 'getById'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);
        
        // API - Criação
        $router->post('/api/fiscal/nfe-entrada/manual', [$nfeEntrada, 'createManual'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.create'])]);
        $router->post('/api/fiscal/nfe-entrada/importar-xml', [$nfeEntrada, 'importarXml'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.create'])]);
        $router->post('/api/fiscal/nfe-entrada/consultar-chave', [$nfeEntrada, 'consultarChave'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);
        
        // API - Processamento
        $router->post('/api/fiscal/nfe-entrada/{id}/processar', [$nfeEntrada, 'processar'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.create'])]);
        
        // API - Detecção automática
        $router->get('/api/fiscal/nfe-entrada/deteccao/config', function(Request $r) {
            $service = new \Modules\Fiscal\Services\NFeDeteccaoService();
            $config = $service->obterConfiguracao();
            return Response::json(['data' => $config]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);
        $router->post('/api/fiscal/nfe-entrada/deteccao/config', [$nfeEntrada, 'configurarDeteccao'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.manage'])]);
        $router->post('/api/fiscal/nfe-entrada/deteccao/executar', [$nfeEntrada, 'executarDeteccao'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.manage'])]);
        
        // API - Webhook (público com validação de token)
        $webhook = new \Modules\Fiscal\Controllers\NFeWebhookController();
        $router->post('/api/fiscal/nfe-entrada/webhook', [$webhook, 'receberWebhook'], []); // Sem autenticação - validação interna via token

        // ==== NOTIFICAÇÕES FISCAIS ====
        
        // GET /api/fiscal/notifications - Lista notificações do usuário
        $router->get('/api/fiscal/notifications', function(Request $request) {
            try {
                $notificationService = new FiscalNotificationService();
                $user = \Gestoreasy\Core\Auth\UserContext::get($request) ?? [];
                $uid = (int)($user['sub'] ?? ($user['id'] ?? 0));
                $userId = $uid > 0 ? $uid : null;
                $status = (string)($request->query['status'] ?? 'all'); // all, unread, read, dismissed
                $limit = min(100, max(1, (int)($request->query['limit'] ?? 50)));
                
                $notifications = $notificationService->getUserNotifications($userId, $status, $limit);
                $unreadCount = $notificationService->getUnreadCount($userId);
                
                return Response::json([
                    'notifications' => $notifications,
                    'unread_count' => $unreadCount,
                    'total' => count($notifications)
                ]);
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha ao buscar notificações', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);

        // POST /api/fiscal/notifications/{id}/read - Marca notificação como lida
        $router->post('/api/fiscal/notifications/{id}/read', function(Request $request) {
            try {
                $notificationService = new FiscalNotificationService();
                $notificationId = (int)($request->params['id'] ?? 0);
                
                $success = $notificationService->markAsRead($notificationId);
                
                if ($success) {
                    return Response::json(['message' => 'Notificação marcada como lida']);
                } else {
                    return Response::json(['error' => 'Notificação não encontrada ou já lida'], 404);
                }
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha ao marcar notificação', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);

        // POST /api/fiscal/notifications/{id}/dismiss - Dispensa notificação
        $router->post('/api/fiscal/notifications/{id}/dismiss', function(Request $request) {
            try {
                $notificationService = new FiscalNotificationService();
                $notificationId = (int)($request->params['id'] ?? 0);
                
                $success = $notificationService->dismiss($notificationId);
                
                if ($success) {
                    return Response::json(['message' => 'Notificação dispensada']);
                } else {
                    return Response::json(['error' => 'Notificação não encontrada'], 404);
                }
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha ao dispensar notificação', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);

        // GET /api/fiscal/notifications/settings - Configurações de notificação do usuário
        $router->get('/api/fiscal/notifications/settings', function(Request $request) {
            try {
                $notificationService = new FiscalNotificationService();
                $user = \Gestoreasy\Core\Auth\UserContext::get($request) ?? [];
                $userId = (int)($user['sub'] ?? ($user['id'] ?? 0));
                if ($userId <= 0) return Response::json(['error' => 'usuário inválido'], 403);
                
                $settings = $notificationService->getUserSettings($userId);
                
                return Response::json($settings);
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha ao buscar configurações', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.read'])]);

        // PUT /api/fiscal/notifications/settings - Atualiza configurações de notificação
        $router->put('/api/fiscal/notifications/settings', function(Request $request) {
            try {
                $notificationService = new FiscalNotificationService();
                $user = \Gestoreasy\Core\Auth\UserContext::get($request) ?? [];
                $userId = (int)($user['sub'] ?? ($user['id'] ?? 0));
                if ($userId <= 0) return Response::json(['error' => 'usuário inválido'], 403);
                $body = is_array($request->body ?? null) ? $request->body : [];
                
                $success = $notificationService->updateUserSettings($userId, $body);
                
                if ($success) {
                    return Response::json(['message' => 'Configurações atualizadas com sucesso']);
                } else {
                    return Response::json(['error' => 'Nenhuma configuração válida fornecida'], 400);
                }
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha ao atualizar configurações', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.manage'])]);

        // POST /api/fiscal/notifications/check-deadlines - Verifica e cria alertas de prazos
        $router->post('/api/fiscal/notifications/check-deadlines', function(Request $request) {
            try {
                $notificationService = new FiscalNotificationService();
                
                $alerts = $notificationService->checkAndCreateDeadlineAlerts();
                $cleanedCount = $notificationService->cleanupExpiredNotifications();
                
                return Response::json([
                    'message' => 'Verificação de prazos concluída',
                    'alerts_created' => count($alerts),
                    'expired_cleaned' => $cleanedCount,
                    'alerts' => $alerts
                ]);
            } catch (\Throwable $e) {
                return Response::json(['error' => 'Falha na verificação de prazos', 'message' => $e->getMessage()], 500);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['fiscal.manage'])]);
    }

    public function name(): string { return 'Fiscal'; }
    public function version(): string { return '0.1.0'; }
    public function dependencies(): array { return ['Vendas']; }
}