<?php
namespace Modules\PDV;

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\Auth\UserContext;
use Gestoreasy\Core\Http\StockPolicyMiddleware;

class Module implements ModuleInterface {
    public function register(Router $router): void {
        // Busca de produtos para PDV com relevância, imagem e localização
        $router->get('/api/pdv/produtos/search', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $q = trim((string)($r->query['q'] ?? ''));
            $page = max(1, (int)($r->query['page'] ?? 1));
            $per = min(50, max(1, (int)($r->query['per_page'] ?? 20)));
            $offset = ($page - 1) * $per;

            // Verificar existência de product_meta
            $hasMeta = false;
            try {
                $chk = $pdo->prepare("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'product_meta'");
                $chk->execute();
                $hasMeta = ((int)($chk->fetch()['c'] ?? 0)) > 0;
            } catch (\Throwable $e) { $hasMeta = false; }

            // WHERE por busca ampla (usa apenas parâmetros nomeados para evitar mistura com posicionais)
            $where = '';
            $whereParams = [];
            if ($q !== '') {
                $like = "%$q%";
                if ($hasMeta) {
                    $where = 'WHERE p.codigo LIKE :w1 OR p.ean LIKE :w2 OR p.ncm LIKE :w3 OR CAST(pm.meta_json AS CHAR) LIKE :w4';
                    $whereParams = [
                        ':w1' => $like,
                        ':w2' => $like,
                        ':w3' => $like,
                        ':w4' => $like,
                    ];
                } else {
                    $where = 'WHERE p.codigo LIKE :w1 OR p.ean LIKE :w2 OR p.ncm LIKE :w3';
                    $whereParams = [
                        ':w1' => $like,
                        ':w2' => $like,
                        ':w3' => $like,
                    ];
                }
            }

            $join = $hasMeta ? 'LEFT JOIN product_meta pm ON pm.product_id = p.id' : '';
            $selectNome = $hasMeta ? ", COALESCE(JSON_UNQUOTE(JSON_EXTRACT(pm.meta_json, '$.nome')), p.codigo) AS nome" : ", p.codigo AS nome";

            // Relevância simples: exato em código > exato em EAN > início do nome > contém no nome > outros
            $relevance = $hasMeta
                ? "(CASE 
                        WHEN p.codigo = :ex_q THEN 100
                        WHEN p.ean = :ex_q THEN 90
                        WHEN JSON_UNQUOTE(JSON_EXTRACT(pm.meta_json, '$.nome')) LIKE :prefix THEN 80
                        WHEN CAST(pm.meta_json AS CHAR) LIKE :like THEN 60
                        ELSE 10 END) AS relevance"
                : "(CASE 
                        WHEN p.codigo = :ex_q THEN 100
                        WHEN p.ean = :ex_q THEN 90
                        WHEN p.codigo LIKE :prefix THEN 50
                        ELSE 10 END) AS relevance";

            // Parâmetros de relevância
            $paramsRelevance = [
                ':ex_q' => $q,
                ':prefix' => $q === '' ? '' : ($q . '%'),
                ':like' => "%$q%",
            ];

            // Consulta paginada com subconsulta para imagem principal
            $sql = "SELECT p.id, p.codigo, p.estoque, p.preco,
                        p.location_corredor, p.location_prateleira, p.location_posicao
                        $selectNome,
                        (SELECT COALESCE(url, CONCAT('/uploads/', filename))
                           FROM product_media pm2
                          WHERE pm2.product_id = p.id
                          ORDER BY pm2.position ASC, pm2.id ASC
                          LIMIT 1) AS image_url,
                        $relevance
                    FROM products p
                    $join
                    $where
                    ORDER BY relevance DESC, nome ASC
                    LIMIT $per OFFSET $offset";

            // Preparar e executar com junção de parâmetros (todos nomeados)
            $stmt = $pdo->prepare($sql);
            // Merge: relevância + where
            foreach ($paramsRelevance as $k => $v) { $stmt->bindValue($k, $v); }
            foreach ($whereParams as $k => $v) { $stmt->bindValue($k, $v); }
            $t0 = microtime(true);
            $stmt->execute();
            $rows = $stmt->fetchAll();
            $elapsed = (microtime(true) - $t0);

            // Montar saída com placeholder de imagem e string de localização
            $out = [];
            foreach ($rows as $row) {
                $img = (string)($row['image_url'] ?? '');
                if ($img === '' || $img === null) { $img = '/assets/no-image.svg'; }
                $locStr = '';
                $c = trim((string)($row['location_corredor'] ?? ''));
                $p = trim((string)($row['location_prateleira'] ?? ''));
                $pos = trim((string)($row['location_posicao'] ?? ''));
                if ($c !== '' || $p !== '' || $pos !== '') {
                    $parts = [];
                    if ($c !== '') $parts[] = 'Corredor ' . $c;
                    if ($p !== '') $parts[] = 'Prateleira ' . $p;
                    if ($pos !== '') $parts[] = 'Posição ' . $pos;
                    $locStr = implode(' / ', $parts);
                }
                $out[] = [
                    'id' => (int)$row['id'],
                    'codigo' => (string)$row['codigo'],
                    'nome' => (string)$row['nome'],
                    'estoque' => (float)$row['estoque'],
                    'preco' => (float)$row['preco'],
                    'image_url' => $img,
                    'location_corredor' => $c,
                    'location_prateleira' => $p,
                    'location_posicao' => $pos,
                    'location' => $locStr,
                ];
            }

            // Cabeçalho de performance simples (ms) para depuração
            $headers = ['X-Query-Time' => (string)round($elapsed * 1000)];
            return new Response(200, ['data' => $out, 'page' => $page, 'per_page' => $per], $headers);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        $router->get('/api/pdv/caixas', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $stmt = $pdo->query("SELECT id, data, status, valor_fechamento FROM cash_closures ORDER BY data DESC LIMIT 50");
            return Response::json(['data' => $stmt->fetchAll()]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Sessão de caixa atual (aberta)
        $router->get('/api/pdv/caixas/sessao-atual', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            try {
                $stmt = $pdo->query("SELECT id, operator_id, opened_at, initial_amount, status FROM cash_sessions WHERE status = 'aberta' AND closed_at IS NULL ORDER BY opened_at DESC LIMIT 1");
                $row = $stmt->fetch();
                return Response::json(['open' => (bool)$row, 'session' => $row ?: null]);
            } catch (\Throwable $e) {
                return new Response(200, ['open' => false, 'session' => null]);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Abrir caixa
        $router->post('/api/pdv/caixas/abrir', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            // Impedir múltiplas sessões abertas
            try {
                $has = $pdo->query("SELECT id FROM cash_sessions WHERE status='aberta' AND closed_at IS NULL LIMIT 1")->fetch();
                if ($has) return new Response(409, ['error' => ['code' => 'session_open_exists', 'message' => 'Já existe um caixa aberto']]);
            } catch (\Throwable $e) { /* tabela pode não existir ainda */ }
            $user = UserContext::get($r) ?? [];
            $operatorId = (int)($user['sub'] ?? 0);
            $initial = (float)($r->body['valor_inicial'] ?? 0);
            try {
                $stmt = $pdo->prepare('INSERT INTO cash_sessions (operator_id, initial_amount, status) VALUES (?,?,?)');
                $stmt->execute([$operatorId ?: null, $initial, 'aberta']);
                $sid = (int)$pdo->lastInsertId();
                AuditLogger::logCreate('cash_sessions', $sid, ['operator_id' => $operatorId, 'initial_amount' => $initial]);
                return Response::json(['ok' => true, 'session_id' => $sid], 201);
            } catch (\Throwable $e) {
                return new Response(500, ['error' => ['code' => 'open_failed', 'message' => $e->getMessage()]]);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Suprimento
        $router->post('/api/pdv/caixas/suprimento', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $amount = (float)($r->body['valor'] ?? 0);
            $reason = trim((string)($r->body['motivo'] ?? 'Suprimento'));
            if ($amount <= 0) return new Response(422, ['error' => ['code' => 'amount_required', 'message' => 'Valor deve ser > 0']]);
            try {
                $sid = $pdo->query("SELECT id FROM cash_sessions WHERE status='aberta' AND closed_at IS NULL ORDER BY opened_at DESC LIMIT 1")->fetch()['id'] ?? null;
                if (!$sid) return new Response(409, ['error' => ['code' => 'no_open_session', 'message' => 'Nenhum caixa aberto']]);
                $ins = $pdo->prepare('INSERT INTO cash_movements (session_id, type, amount, reason) VALUES (?,?,?,?)');
                $ins->execute([(int)$sid, 'suprimento', $amount, $reason]);
                AuditLogger::logCreate('cash_movements', (int)$pdo->lastInsertId(), ['type' => 'suprimento', 'amount' => $amount, 'session_id' => (int)$sid]);
                return Response::json(['ok' => true]);
            } catch (\Throwable $e) {
                return new Response(500, ['error' => ['code' => 'supply_failed', 'message' => $e->getMessage()]]);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Sangria
        $router->post('/api/pdv/caixas/sangria', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $amount = (float)($r->body['valor'] ?? 0);
            $reason = trim((string)($r->body['motivo'] ?? 'Sangria'));
            if ($amount <= 0) return new Response(422, ['error' => ['code' => 'amount_required', 'message' => 'Valor deve ser > 0']]);
            try {
                $sid = $pdo->query("SELECT id FROM cash_sessions WHERE status='aberta' AND closed_at IS NULL ORDER BY opened_at DESC LIMIT 1")->fetch()['id'] ?? null;
                if (!$sid) return new Response(409, ['error' => ['code' => 'no_open_session', 'message' => 'Nenhum caixa aberto']]);
                $ins = $pdo->prepare('INSERT INTO cash_movements (session_id, type, amount, reason) VALUES (?,?,?,?)');
                $ins->execute([(int)$sid, 'sangria', $amount, $reason]);
                AuditLogger::logCreate('cash_movements', (int)$pdo->lastInsertId(), ['type' => 'sangria', 'amount' => $amount, 'session_id' => (int)$sid]);
                return Response::json(['ok' => true]);
            } catch (\Throwable $e) {
                return new Response(500, ['error' => ['code' => 'withdraw_failed', 'message' => $e->getMessage()]]);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Fechar caixa
        $router->post('/api/pdv/caixas/fechar', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            try {
                $row = $pdo->query("SELECT id, opened_at, initial_amount FROM cash_sessions WHERE status='aberta' AND closed_at IS NULL ORDER BY opened_at DESC LIMIT 1")->fetch();
                if (!$row) return new Response(409, ['error' => ['code' => 'no_open_session', 'message' => 'Nenhum caixa aberto']]);
                $sid = (int)$row['id'];
                $openedAt = (string)$row['opened_at'];
                $initial = (float)$row['initial_amount'];
                // Somar vendas no período (aproximação)
                $sumSales = $pdo->prepare('SELECT COALESCE(SUM(valor_total),0) AS total FROM pos_sales WHERE created_at >= ?');
                $sumSales->execute([$openedAt]);
                $totalSales = (float)($sumSales->fetch()['total'] ?? 0);
                // Somar movimentos
                $sumMov = $pdo->prepare("SELECT type, COALESCE(SUM(amount),0) AS total FROM cash_movements WHERE session_id = ? GROUP BY type");
                $sumMov->execute([$sid]);
                $byType = [];
                foreach ($sumMov->fetchAll() as $m) { $byType[$m['type']] = (float)$m['total']; }
                $supr = $byType['suprimento'] ?? 0.0;
                $sang = $byType['sangria'] ?? 0.0;
                // Fechamento básico: inicial + suprimentos + vendas - sangrias
                $closing = $initial + $supr + $totalSales - $sang;
                $upd = $pdo->prepare("UPDATE cash_sessions SET status='fechado', closed_at = NOW(), closing_amount = ? WHERE id = ?");
                $upd->execute([$closing, $sid]);
                // Compatibilidade: registrar em cash_closures por data
                try {
                    $dia = date('Y-m-d');
                    $cc = $pdo->prepare('INSERT INTO cash_closures (data, status, valor_fechamento) VALUES (?,?,?)');
                    $cc->execute([$dia, 'fechado', $closing]);
                    $closureId = (int)$pdo->lastInsertId();
                    AuditLogger::logCreate('cash_closures', $closureId, [], ['endpoint' => 'pdv.cash.close.session', 'session_id' => $sid, 'valor_fechamento' => $closing]);
                } catch (\Throwable $e) { /* tabela pode não existir */ }
                AuditLogger::logUpdate('cash_sessions', $sid, ['closing_amount' => $closing, 'status' => 'fechado']);
                return Response::json(['ok' => true, 'session_id' => $sid, 'closing_amount' => $closing]);
            } catch (\Throwable $e) {
                return new Response(500, ['error' => ['code' => 'close_failed', 'message' => $e->getMessage()]]);
            }
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Registrar uma venda de PDV em uma única chamada
        // Body esperado: { items: [{ product_id: number, quantidade: number }], cliente_id?: number }
        $router->post('/api/pdv/venda', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $data = $r->body;
            $items = is_array($data['items'] ?? null) ? $data['items'] : [];
            $clienteId = (int)($data['cliente_id'] ?? 0);
            if (count($items) === 0) {
                return new Response(422, ['error' => ['code' => 'items_required', 'message' => 'Lista de itens é obrigatória']]);
            }
            // Se cliente for informado, validar existência
            $clienteNome = null;
            if ($clienteId > 0) {
                $cStmt = $pdo->prepare('SELECT id, nome FROM clients WHERE id = ? LIMIT 1');
                $cStmt->execute([$clienteId]);
                $cli = $cStmt->fetch();
                if (!$cli) {
                    return new Response(422, ['error' => ['code' => 'invalid_client', 'message' => 'Cliente não encontrado']]);
                }
                $clienteNome = (string)($cli['nome'] ?? '');
            }
            // Validação básica e cálculo de total
            $totalOriginal = 0.0; // Total sem descontos
            $summary = [];
            foreach ($items as $it) {
                $pid = (int)($it['product_id'] ?? 0);
                $qtd = (float)($it['quantidade'] ?? 0);
                if ($pid <= 0 || $qtd <= 0) {
                    return new Response(422, ['error' => ['code' => 'invalid_item', 'message' => 'Item inválido: product_id e quantidade > 0 requeridos']]);
                }
                $st = $pdo->prepare('SELECT id, codigo, preco FROM products WHERE id = ? LIMIT 1');
                $st->execute([$pid]);
                $prod = $st->fetch();
                if (!$prod) return new Response(404, ['error' => ['code' => 'product_not_found', 'message' => 'Produto não encontrado']]);
                
                // Preço pode ser sobrescrito
                $preco = isset($it['preco']) ? (float)$it['preco'] : (float)$prod['preco'];
                
                // Desconto por item
                $descontoPercent = (float)($it['desconto_percent'] ?? 0);
                $descontoValor = (float)($it['desconto_valor'] ?? 0);
                $linhaTotalOriginal = $qtd * $preco;
                $linhaTotal = $linhaTotalOriginal;
                $linhaTotal = $linhaTotal * (1 - ($descontoPercent / 100));
                $linhaTotal = $linhaTotal - $descontoValor;
                $linhaTotal = max(0, $linhaTotal); // Não permitir negativo
                
                $totalOriginal += $linhaTotalOriginal;
                $summary[] = [
                    'product_id' => $pid,
                    'codigo' => $prod['codigo'],
                    'quantidade' => $qtd,
                    'preco' => $preco,
                    'desconto_percent' => $descontoPercent,
                    'desconto_valor' => $descontoValor,
                    'total_original' => $linhaTotalOriginal,
                    'total' => $linhaTotal
                ];
            }
            
            // Calcular total com descontos de itens aplicados
            $total = 0.0;
            foreach ($summary as $s) {
                $total += (float)$s['total'];
            }

            // Aplicar descontos/acréscimos globais
            $descontoGlobalPercent = (float)($data['desconto_percent'] ?? 0);
            $descontoGlobalValor = (float)($data['desconto_valor'] ?? 0);
            $acrescimoGlobalPercent = (float)($data['acrescimo_percent'] ?? 0);
            $acrescimoGlobalValor = (float)($data['acrescimo_valor'] ?? 0);
            
            // Validar e aplicar cupom se fornecido
            $cupomCodigo = trim((string)($data['cupom'] ?? ''));
            $cupomDesconto = 0;
            if ($cupomCodigo !== '') {
                $stmt = $pdo->prepare('
                    SELECT * FROM pos_coupons
                    WHERE codigo = ? AND ativo = 1
                    AND (validade_inicio IS NULL OR validade_inicio <= NOW())
                    AND (validade_fim IS NULL OR validade_fim >= NOW())
                    AND (quantidade_maxima = 0 OR quantidade_usada < quantidade_maxima)
                    LIMIT 1
                ');
                $stmt->execute([$cupomCodigo]);
                $cupom = $stmt->fetch();
                
                if ($cupom) {
                    if ($cupom['tipo'] === 'desconto') {
                        if ($cupom['percent'] > 0) {
                            $cupomDesconto = $total * ($cupom['percent'] / 100);
                        } else {
                            $cupomDesconto = (float)$cupom['valor'];
                        }
                        // Aplicar limites
                        if ($cupom['valor_minimo'] > 0 && $total < $cupom['valor_minimo']) {
                            return new Response(422, ['error' => ['code' => 'cupom_valor_minimo', 'message' => 'Cupom requer valor mínimo de R$ ' . number_format($cupom['valor_minimo'], 2, ',', '.')]]);
                        }
                        if ($cupom['valor_maximo'] > 0 && $cupomDesconto > $cupom['valor_maximo']) {
                            $cupomDesconto = (float)$cupom['valor_maximo'];
                        }
                    }
                    
                    // Marcar cupom como usado
                    $stmt = $pdo->prepare('UPDATE pos_coupons SET quantidade_usada = quantidade_usada + 1 WHERE id = ?');
                    $stmt->execute([$cupom['id']]);
                }
            }
            
            // Aplicar descontos
            $total = $total * (1 - ($descontoGlobalPercent / 100));
            $total = $total - $descontoGlobalValor - $cupomDesconto;
            
            // Aplicar acréscimos
            $total = $total * (1 + ($acrescimoGlobalPercent / 100));
            $total = $total + $acrescimoGlobalValor;
            $total = max(0, $total); // Não permitir negativo
            
            $valorFinal = $total;

            // Inserir venda POS
            $numero = 'PDV-' . date('YmdHis') . '-' . rand(1000, 9999);
            $pdo->beginTransaction();
            try {
                $ins = $pdo->prepare('
                    INSERT INTO pos_sales (
                        numero, valor_total, desconto_percent, desconto_valor,
                        acrescimo_percent, acrescimo_valor, valor_final, cliente_id, observacoes
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
                ');
                $ins->execute([
                    $numero,
                    $totalOriginal, // Valor original sem descontos
                    $descontoGlobalPercent,
                    $descontoGlobalValor + $cupomDesconto,
                    $acrescimoGlobalPercent,
                    $acrescimoGlobalValor,
                    $valorFinal,
                    $clienteId > 0 ? $clienteId : null,
                    $data['observacoes'] ?? null
                ]);
                $saleId = (int)$pdo->lastInsertId();
            } catch (\Throwable $e) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                return new Response(500, ['error' => ['code' => 'sale_insert_failed', 'message' => $e->getMessage()]]);
            }

            // Persistir itens da venda
            try {
                $itemIns = $pdo->prepare('
                    INSERT INTO pos_sale_items (
                        sale_id, product_id, quantidade, preco, total,
                        desconto_percent, desconto_valor, valor_final
                    ) VALUES (?,?,?,?,?,?,?,?)
                ');
                foreach ($summary as $s) {
                    $itemIns->execute([
                        $saleId,
                        (int)$s['product_id'],
                        (float)$s['quantidade'],
                        (float)$s['preco'],
                        (float)$s['total_original'], // Total original sem desconto
                        (float)$s['desconto_percent'],
                        (float)$s['desconto_valor'],
                        (float)$s['total'] // Total final com desconto
                    ]);
                }
            } catch (\Throwable $e) {
                // Se a tabela ainda não existir, seguimos sem persistir itens
            }

            // Registrar pagamentos se fornecidos
            if (isset($data['pagamentos']) && is_array($data['pagamentos'])) {
                try {
                    $paymentIns = $pdo->prepare('
                        INSERT INTO pos_sale_payments (
                            sale_id, forma_pagamento, valor, troco, parcela, total_parcelas
                        ) VALUES (?, ?, ?, ?, ?, ?)
                    ');
                    foreach ($data['pagamentos'] as $pagamento) {
                        $paymentIns->execute([
                            $saleId,
                            $pagamento['forma_pagamento'] ?? 'dinheiro',
                            (float)($pagamento['valor'] ?? 0),
                            (float)($pagamento['troco'] ?? 0),
                            (int)($pagamento['parcela'] ?? 1),
                            (int)($pagamento['total_parcelas'] ?? 1)
                        ]);
                    }
                } catch (\Throwable $e) {
                    // Ignorar se tabela não existir
                }
            }

            // Registrar cupom usado se aplicado
            if ($cupomCodigo !== '' && isset($cupom) && $cupomDesconto > 0) {
                try {
                    $cupomIns = $pdo->prepare('
                        INSERT INTO pos_sale_coupons (
                            sale_id, codigo, tipo, valor, percent, usado, usado_em
                        ) VALUES (?, ?, ?, ?, ?, 1, NOW())
                    ');
                    $cupomIns->execute([
                        $saleId,
                        $cupomCodigo,
                        $cupom['tipo'],
                        $cupomDesconto,
                        $cupom['percent']
                    ]);
                } catch (\Throwable $e) {
                    // Ignorar se tabela não existir
                }
            }

            // Política de estoque: usar atributo do Request (via middleware), com fallback ao DB
            $attr = $r->get('stock.allow_negative', null);
            if ($attr === null) {
                $allowNeg = false;
                try { $cfg = $pdo->query('SELECT permitir_estoque_negativo FROM inventory_settings LIMIT 1')->fetch(); $allowNeg = ((int)($cfg['permitir_estoque_negativo'] ?? 0)) === 1; } catch (\Throwable $e) { $allowNeg = false; }
            } else {
                $allowNeg = (bool)$attr;
            }

            // Movimentar e atualizar estoque (saída)
            $negSale = false;
            foreach ($summary as $s) {
                $pid = (int)$s['product_id'];
                $qtd = (float)$s['quantidade'];
                // Consultar estoque atual
                $st = $pdo->prepare('SELECT estoque FROM products WHERE id = ? LIMIT 1');
                $st->execute([$pid]);
                $row = $st->fetch();
                if (!$row) { if ($pdo->inTransaction()) $pdo->rollBack(); return new Response(404, ['error' => ['code' => 'product_not_found', 'message' => 'Produto não encontrado', 'product_id' => $pid]]); }
                $before = (float)($row['estoque'] ?? 0);
                $after = $before - $qtd;
                if (!$allowNeg && $after < 0) {
                    // Log de tentativa bloqueada
                    try { AuditLogger::log('stock_policy_violation', 'pos_sales', $saleId, null, null, ['product_id' => $pid, 'available' => $before, 'required' => $qtd, 'endpoint' => 'pdv.venda.create']); } catch (\Throwable $e) {}
                    if ($pdo->inTransaction()) $pdo->rollBack();
                    return new Response(422, ['error' => ['code' => 'insufficient_stock', 'message' => 'Estoque insuficiente para realizar a venda. Disponível: ' . $before . ' unidades', 'product_id' => $pid, 'available' => $before, 'required' => $qtd]]);
                }
                if ($after < 0) { $negSale = true; }
                // Tentar SP para ajuste atômico; fallback
                $spOk = false;
                try {
                    $call = $pdo->prepare('CALL sp_update_product_stock(?,?,?,?,?,?)');
                    $call->execute([$pid, -$qtd, 'pos_sales', $saleId, $numero, $allowNeg ? 1 : 0]);
                    $spOk = true;
                } catch (\Throwable $e) { $spOk = false; }
                if (!$spOk) {
                    $upd = $pdo->prepare('UPDATE products SET estoque = ? WHERE id = ?');
                    $upd->execute([$after, $pid]);
                    try {
                        $mov = $pdo->prepare('INSERT INTO inventory_movements (product_id, tipo, quantidade, motivo, stock_before, stock_after, origin_type, origin_id, origin_number) VALUES (?,?,?,?,?,?,?,?,?)');
                        $mov->execute([$pid, 'saida_pdv', -$qtd, 'Venda PDV', $before, $after, 'pos_sales', $saleId, $numero]);
                    } catch (\Throwable $e) { /* ignorar */ }
                }
            }

            // Commit após movimentação
            if ($pdo->inTransaction()) $pdo->commit();

            // Auditoria com user_id do contexto
            $user = UserContext::get($r) ?? [];
            AuditLogger::logCreate(
                'pos_sales',
                $saleId,
                ['numero' => $numero, 'valor_total' => $totalOriginal, 'valor_final' => $valorFinal, 'cliente_id' => ($clienteId>0?$clienteId:null)],
                ['items' => $summary, 'endpoint' => 'pdv.venda.create', 'user_id' => ($user['sub'] ?? null)]
            );

            // Retornar indicador de venda com estoque negativo, se ocorreu
            return Response::json([
                'ok' => true,
                'id' => $saleId,
                'numero' => $numero,
                'valor_total' => $totalOriginal,
                'valor_final' => $valorFinal,
                'desconto_total' => $descontoGlobalValor + $cupomDesconto,
                'acrescimo_total' => $acrescimoGlobalValor,
                'cliente_id' => ($clienteId>0?$clienteId:null),
                'cliente_nome' => $clienteNome,
                'cupom_aplicado' => $cupomCodigo !== '' && isset($cupom),
                'negative_stock' => $negSale
            ], 201);
        }, [AuthMiddleware::requireAuth(), StockPolicyMiddleware::inject(), Rbac::requirePermissions(['vendas.create'])]);

        // Listar vendas PDV
        $router->get('/api/pdv/vendas', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $stmt = $pdo->query('SELECT ps.id, ps.numero, ps.valor_total, ps.created_at, ps.cliente_id, c.nome AS cliente_nome FROM pos_sales ps LEFT JOIN clients c ON ps.cliente_id = c.id ORDER BY ps.id DESC LIMIT 50');
            AuditLogger::logRead('pos_sales', null, ['endpoint' => 'pdv.sales.list']);
            return Response::json(['data' => $stmt->fetchAll()]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Detalhar uma venda PDV com itens persistidos
        $router->get('/api/pdv/vendas/{id}', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $params = $r->get('params', []);
            $id = isset($params[0]) ? (int)$params[0] : 0;
            if ($id <= 0) {
                return new Response(400, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
            }

            // Buscar a venda
            $stmt = $pdo->prepare('SELECT ps.id, ps.numero, ps.valor_total, ps.created_at, ps.cliente_id, c.nome AS cliente_nome FROM pos_sales ps LEFT JOIN clients c ON ps.cliente_id = c.id WHERE ps.id = ? LIMIT 1');
            $stmt->execute([$id]);
            $sale = $stmt->fetch();
            if (!$sale) {
                return new Response(404, ['error' => ['code' => 'sale_not_found', 'message' => 'Venda não encontrada']]);
            }

            // Itens persistidos em pos_sale_items
            $items = [];
            try {
                $itStmt = $pdo->prepare('SELECT product_id, quantidade, preco, total FROM pos_sale_items WHERE sale_id = ? ORDER BY id ASC');
                $itStmt->execute([$id]);
                $items = $itStmt->fetchAll();
            } catch (\Throwable $e) {
                // Fallback para auditoria caso a tabela não exista
                $logs = AuditLogger::getAuditTrail('pos_sales', $id, 1);
                if (!empty($logs)) {
                    $meta = $logs[0]['metadata'] ?? null;
                    if (is_array($meta) && isset($meta['items']) && is_array($meta['items'])) {
                        $items = $meta['items'];
                    }
                }
            }

            AuditLogger::logRead('pos_sales', $id, ['endpoint' => 'pdv.sales.detail']);
            return Response::json(['data' => ['sale' => $sale, 'items' => $items]]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Listar vendas/pedidos não faturados para "puxar" no PDV
        $router->get('/api/pdv/vendas-nao-faturadas', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $page = max(1, (int)($r->query['page'] ?? 1));
            $per = min(50, max(1, (int)($r->query['per_page'] ?? 20)));
            $offset = ($page - 1) * $per;
            
            // Buscar pedidos não faturados (status 'aberto' ou 'confirmado')
            $sql = "
                SELECT so.id, so.numero, so.cliente_id, so.valor_total, so.status, so.created_at,
                       c.nome as cliente_nome
                FROM sales_orders so
                LEFT JOIN clients c ON so.cliente_id = c.id
                WHERE so.status IN ('aberto', 'confirmado')
                ORDER BY so.created_at DESC
                LIMIT $per OFFSET $offset
            ";
            $stmt = $pdo->prepare($sql);
            $stmt->execute();
            $rows = $stmt->fetchAll();
            
            // Contar total
            $countStmt = $pdo->prepare("SELECT COUNT(*) AS c FROM sales_orders WHERE status IN ('aberto', 'confirmado')");
            $countStmt->execute();
            $total = (int)$countStmt->fetch()['c'];
            $totalPages = (int)ceil($total / $per);
            
            AuditLogger::logRead('sales_orders', null, ['endpoint' => 'pdv.vendas_nao_faturadas', 'page' => $page]);
            return Response::json([
                'data' => $rows, 
                'page' => $page, 
                'per_page' => $per, 
                'total' => $total, 
                'total_pages' => $totalPages
            ]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // Obter detalhes de uma venda não faturada para "puxar" no PDV
        $router->get('/api/pdv/vendas-nao-faturadas/{id}', function (Request $r) {
            $pdo = ConnectionManager::forTenant();
            $params = $r->get('params', []);
            $id = (int)($params[0] ?? 0);
            if ($id <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
            
            // Buscar pedido
            $stmt = $pdo->prepare("
                SELECT so.id, so.numero, so.cliente_id, so.valor_total, so.status, so.created_at,
                       c.nome as cliente_nome, c.cpf_cnpj
                FROM sales_orders so
                LEFT JOIN clients c ON so.cliente_id = c.id
                WHERE so.id = ? AND so.status IN ('aberto', 'confirmado')
                LIMIT 1
            ");
            $stmt->execute([$id]);
            $order = $stmt->fetch();
            if (!$order) return new Response(404, ['error' => ['code' => 'not_found', 'message' => 'Venda não encontrada ou já faturada']]);
            
            // Buscar itens
            $items = [];
            try {
                $itStmt = $pdo->prepare("
                    SELECT soi.product_id, soi.quantidade, soi.preco, soi.total,
                           p.codigo, p.descricao
                    FROM sales_order_items soi
                    LEFT JOIN products p ON soi.product_id = p.id
                    WHERE soi.order_id = ?
                    ORDER BY soi.id ASC
                ");
                $itStmt->execute([$id]);
                $items = $itStmt->fetchAll();
            } catch (\Throwable $e) {
                // Sem itens persistidos
            }
            
            AuditLogger::logRead('sales_orders', $id, ['endpoint' => 'pdv.vendas_nao_faturadas.detail']);
            return Response::json(['data' => ['order' => $order, 'items' => $items]]);
        }, [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.read'])]);

        // ==== Gerenciamento de Cupons (Admin) ====
        $couponsCtrl = new \Modules\PDV\Controllers\CouponsController();
        $router->get('/api/pdv/cupons/admin', [$couponsCtrl, 'list'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.manage'])]);
        $router->post('/api/pdv/cupons/admin', [$couponsCtrl, 'create'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.manage'])]);
        $router->put('/api/pdv/cupons/admin/{id}', [$couponsCtrl, 'update'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.manage'])]);
        $router->delete('/api/pdv/cupons/admin/{id}', [$couponsCtrl, 'delete'], [AuthMiddleware::requireAuth(), Rbac::requirePermissions(['pdv.manage'])]);
    }
    public function name(): string { return 'PDV'; }
    public function version(): string { return '0.1.0'; }
    public function dependencies(): array { return ['Vendas']; }
}