<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Core\Auth;
use App\Core\Controller;
use App\Core\Database;
use App\Core\Permission;
use App\Core\Response;
use App\Core\Session;
use App\Services\AuditService;
use App\Services\DocumentNumberService;
use App\Services\ExchangeRateService;
use App\Services\SaleDocumentTypeService as SaleType;
use App\Services\SalePricingService;
use App\Services\StockMovementService;
use App\Services\StockReservationService;
use PDO;
use RuntimeException;
use Throwable;

final class SaleDocumentController extends Controller
{
    public function index(): void
    {
        $companyId = (int)Auth::companyId();
        $type = strtoupper(trim((string)($_GET['type'] ?? '')));
        $status = strtoupper(trim((string)($_GET['status'] ?? '')));
        $q = trim((string)($_GET['q'] ?? ''));
        $customerId = (int)($_GET['customer_id'] ?? 0);

        $this->requireAnySalesViewPermission();

        $sql =
            'SELECT sd.*, c.business_name AS customer_name, b.name AS branch_name, w.name AS warehouse_name,
                    src.document_number AS source_document_number,
                    src.document_type AS source_document_type
             FROM sale_documents sd
             INNER JOIN customers c ON c.id = sd.customer_id
             LEFT JOIN commercial_conditions cc ON cc.id = c.commercial_condition_id
             INNER JOIN branches b ON b.id = sd.branch_id
             LEFT JOIN warehouses w ON w.id = sd.warehouse_id
             LEFT JOIN sale_documents src ON src.id = ' . ($this->hasSaleDocumentColumn(Database::pdo(), 'source_sale_document_id') ? 'sd.source_sale_document_id' : 'NULL') . '
             WHERE sd.company_id = :company_id';
        $params = ['company_id' => $companyId];

        if ($type !== '' && in_array($type, SaleType::allKnownTypes(), true)) {
            $this->requireViewPermission($type);
            $sql .= ' AND sd.document_type = :document_type';
            $params['document_type'] = $type;
        }
        if ($status !== '') {
            $sql .= ' AND sd.status = :status';
            $params['status'] = $status;
        }
        if ($customerId > 0) {
            $sql .= ' AND sd.customer_id = :customer_id';
            $params['customer_id'] = $customerId;
        }
        if ($q !== '') {
            $sql .= ' AND (sd.document_number LIKE :q_doc OR c.business_name LIKE :q_customer OR c.cuit LIKE :q_cuit)';
            $params['q_doc'] = '%' . $q . '%';
            $params['q_customer'] = '%' . $q . '%';
            $params['q_cuit'] = '%' . $q . '%';
        }

        $sql .= ' ORDER BY sd.issue_date DESC, sd.id DESC LIMIT 300';
        $stmt = Database::pdo()->prepare($sql);
        $stmt->execute($params);

        $this->view('sales/index', [
            'title' => 'Documentos comerciales',
            'documents' => $stmt->fetchAll(PDO::FETCH_ASSOC),
            'type' => $type,
            'status' => $status,
            'q' => $q,
            'customerId' => $customerId,
            'documentTypes' => SaleType::labels(),
        ]);
    }

    public function create(): void
    {
        $this->requireAnySalesCreatePermission();

        $companyId = (int)Auth::companyId();
        $rate = (new ExchangeRateService())->latest($companyId, 'MANUAL_VENTA');
        if (!$rate) {
            Session::flash('error', 'Antes de vender tenés que cargar un tipo de cambio MANUAL_VENTA.');
            $this->redirect('/exchange-rates/create');
        }

        $selectedCustomerId = (int)($_GET['customer_id'] ?? 0);
        $this->view('sales/create', $this->formData('Nuevo documento comercial', $rate, $selectedCustomerId));
    }

    public function store(): void
    {
        $this->csrf();
        $this->saveDocument(null);
    }

    public function edit(): void
    {
        $id = (int)($_GET['id'] ?? 0);
        $companyId = (int)Auth::companyId();
        $pdo = Database::pdo();
        $doc = $this->loadDocument($pdo, $companyId, $id);
        if (!$doc) {
            http_response_code(404);
            $this->view('errors/404', ['title' => 'Documento no encontrado']);
            return;
        }
        $this->requireCreatePermission((string)$doc['document_type']);
        $editableReservedBudget = in_array((string)$doc['document_type'], [SaleType::PRESUPUESTO, SaleType::LEGACY_PROFORMA_RESERVA], true) && (string)$doc['status'] === 'RESERVED';
        if ((string)$doc['status'] !== 'DRAFT' && !$editableReservedBudget) {
            Session::flash('error', 'Solo se pueden editar documentos en borrador o presupuestos reservados.');
            $this->redirect('/sales-documents/show?id=' . $id);
        }
        $rate = [
            'id' => $doc['exchange_rate_id'],
            'rate' => $doc['exchange_rate_value'],
            'source' => $doc['exchange_rate_source'],
            'rate_date' => $doc['issue_date'],
        ];
        $data = $this->formData('Editar ' . (string)$doc['document_number'], $rate, (int)$doc['customer_id']);
        $data['mode'] = 'edit';
        $data['document'] = $doc;
        $data['items'] = $this->documentItemsForEdit($pdo, $id);
        $this->view('sales/create', $data);
    }

    public function update(): void
    {
        $this->csrf();
        $id = (int)$this->input('id', 0);
        $this->saveDocument($id);
    }

    private function saveDocument(?int $editingId): void
    {
        $companyId = (int)Auth::companyId();
        $branchId = (int)Auth::branchId();
        $userId = (int)Auth::id();

        $documentType = strtoupper($this->stringInput('document_type'));
        if (!in_array($documentType, SaleType::activeTypes(), true)) {
            Session::flash('error', 'Tipo de documento inválido.');
            $this->oldInput($_POST);
            $this->redirect($editingId ? '/sales-documents/edit?id=' . $editingId : '/sales-documents/create');
        }
        $this->requireCreatePermission($documentType);

        $customerId = (int)$this->input('customer_id', 0);
        $warehouseId = (int)$this->input('warehouse_id', 0);
        $issueDate = $this->stringInput('issue_date', date('Y-m-d'));
        $notes = $this->stringInput('notes');
        $productIds = $_POST['product_id'] ?? [];
        $quantities = $_POST['quantity'] ?? [];
        $discounts = $_POST['discount_percent'] ?? [];
        $documentDiscountPercent = $this->dec($this->stringInput('document_discount_percent', '0'));
        $documentDiscountAmount = $this->dec($this->stringInput('document_discount_amount_ars', '0'));

        $errors = [];
        if ($customerId <= 0) { $errors[] = 'Seleccioná un cliente.'; }
        if ($warehouseId <= 0) { $errors[] = 'Seleccioná un depósito.'; }
        if (!$this->validDate($issueDate)) { $errors[] = 'La fecha no es válida.'; }
        if (!is_array($productIds) || count($productIds) === 0) { $errors[] = 'Agregá al menos un producto o servicio.'; }
        if ($documentDiscountPercent < 0 || $documentDiscountPercent > 100) { $errors[] = 'El descuento general porcentual debe estar entre 0 y 100.'; }
        if ($documentDiscountAmount < 0) { $errors[] = 'El descuento general en pesos no puede ser negativo.'; }

        if ($errors !== []) {
            Session::flash('error', implode(' ', $errors));
            $this->oldInput($_POST);
            $this->redirect($editingId ? '/sales-documents/edit?id=' . $editingId : '/sales-documents/create');
        }

        try {
            $saleDocumentId = Database::transaction(function (PDO $pdo) use ($editingId, $companyId, $branchId, $userId, $documentType, $customerId, $warehouseId, $issueDate, $notes, $productIds, $quantities, $discounts, $documentDiscountPercent, $documentDiscountAmount): int {
                $existing = null;
                $wasReservedBudget = false;
                if ($editingId !== null) {
                    $existing = $this->lockSaleDocument($pdo, $companyId, $editingId);
                    $existingType = (string)$existing['document_type'];
                    $existingStatus = (string)$existing['status'];
                    $wasReservedBudget = in_array($existingType, [SaleType::PRESUPUESTO, SaleType::LEGACY_PROFORMA_RESERVA], true) && $existingStatus === 'RESERVED';
                    if ($existingStatus !== 'DRAFT' && !$wasReservedBudget) {
                        throw new RuntimeException('Solo se pueden editar documentos en borrador o presupuestos reservados.');
                    }
                    if ($wasReservedBudget && $documentType !== SaleType::PRESUPUESTO) {
                        throw new RuntimeException('Un presupuesto reservado solo puede editarse manteniéndolo como Presupuesto. Para generar una venta usá la acción Convertir.');
                    }
                    $this->requireCreatePermission($existingType);
                }

                $customer = $this->lockCustomer($pdo, $companyId, $customerId);
                $this->validateWarehouse($pdo, $companyId, $branchId, $warehouseId);

                $exchangeService = new ExchangeRateService();
                $exchangeRate = $exchangeService->latestSaleRateOrFail($companyId); // MANUAL_VENTA: solo convierte precios a ARS en POS/comprobante.
                $accountExchangeRate = $exchangeService->latestCustomerAccountRateOrFail($companyId); // BNA comprador: dolariza la cuenta corriente.
                $rateValue = (float)$exchangeRate['rate'];
                $accountRateValue = (float)$accountExchangeRate['rate'];
                $priceListId = (new SalePricingService())->customerPriceListId($pdo, $companyId, $customerId);

                $lines = $this->buildLines($pdo, $companyId, $customerId, $priceListId, $productIds, $quantities, $discounts, $rateValue);
                if ($lines === []) {
                    throw new RuntimeException('Agregá al menos una línea válida.');
                }

                $totals = $this->totals($lines, $rateValue, $accountRateValue, $documentDiscountPercent, $documentDiscountAmount);
                $lines = $this->applyDocumentDiscountToMargins($lines, $totals);
                if ($documentType === SaleType::VENTA_MOSTRADOR) {
                    $this->assertStockAvailableForLines($pdo, $companyId, $branchId, $warehouseId, $lines, 'La venta mostrador solo puede guardarse si hay stock disponible.');
                }

                if ($editingId === null) {
                    $number = (new DocumentNumberService())->nextUsingPdo($pdo, $companyId, $branchId, SaleType::code($documentType), SaleType::prefix($documentType));
                    $saleDocumentId = $this->insertSaleDocument($pdo, $companyId, $branchId, $warehouseId, $customerId, $priceListId, $documentType, $number, $issueDate, (int)$exchangeRate['id'], $rateValue, (string)$exchangeRate['source'], (int)$accountExchangeRate['id'], $accountRateValue, (string)$accountExchangeRate['source'], $totals, $notes, $userId, null);
                } else {
                    $saleDocumentId = $editingId;
                    $number = (string)($existing['document_number'] ?? $this->lockSaleDocument($pdo, $companyId, $editingId)['document_number']);
                    if ($wasReservedBudget && $existing !== null) {
                        (new StockReservationService())->releaseSaleDocument($pdo, $companyId, (int)$existing['branch_id'], (int)$existing['warehouse_id'], (int)$existing['id']);
                    }
                    $this->updateSaleDocument($pdo, $saleDocumentId, $warehouseId, $customerId, $priceListId, $documentType, $issueDate, (int)$exchangeRate['id'], $rateValue, (string)$exchangeRate['source'], (int)$accountExchangeRate['id'], $accountRateValue, (string)$accountExchangeRate['source'], $totals, $notes);
                    $pdo->prepare('DELETE FROM sale_document_items WHERE sale_document_id = :id')->execute(['id' => $saleDocumentId]);
                }

                $this->insertItems($pdo, $saleDocumentId, $lines);
                if ($editingId !== null && $wasReservedBudget) {
                    (new StockReservationService())->reserveSaleDocument($pdo, $companyId, $branchId, $warehouseId, $saleDocumentId, $userId);
                    $pdo->prepare('UPDATE sale_documents SET status = "RESERVED" WHERE id = :id')->execute(['id' => $saleDocumentId]);
                }

                (new AuditService())->log('ventas', $editingId === null ? 'crear' : 'editar', 'sale_documents', $saleDocumentId, null, [
                    'document_number' => $number,
                    'document_type' => $documentType,
                    'customer' => $customer['business_name'],
                    'total_ars' => $totals['total_ars'],
                ]);

                return $saleDocumentId;
            });

            Session::flash('success', $editingId === null ? 'Documento creado en borrador.' : 'Documento actualizado.');
            $this->redirect('/sales-documents/show?id=' . $saleDocumentId);
        } catch (Throwable $e) {
            Session::flash('error', $this->errorMessage($e));
            $this->oldInput($_POST);
            $this->redirect($editingId ? '/sales-documents/edit?id=' . $editingId : '/sales-documents/create');
        }
    }

    public function show(): void
    {
        $id = (int)($_GET['id'] ?? 0);
        $data = $this->loadDocumentForView($id);
        $this->requireViewPermission((string)$data['document']['document_type']);

        $this->view('sales/show', [
            'title' => 'Documento ' . $data['document']['document_number'],
            'document' => $data['document'],
            'items' => $data['items'],
            'accountMovements' => $data['accountMovements'],
            'receiptAllocations' => $data['receiptAllocations'],
            'paymentAllocations' => $data['receiptAllocations'],
            'deliveryNotes' => $data['deliveryNotes'],
            'customerReturns' => $data['customerReturns'],
            'unappliedReceipts' => $data['unappliedReceipts'],
            'outstandingUsd' => $data['outstandingUsd'],
            'sourceDocument' => $data['sourceDocument'],
            'generatedDocuments' => $data['generatedDocuments'],
            'typeLabels' => SaleType::labels(),
        ]);
    }

    public function approve(): void
    {
        $this->csrf();
        $id = (int)$this->input('id', 0);
        $userId = (int)Auth::id();
        $companyId = (int)Auth::companyId();

        try {
            Database::transaction(function (PDO $pdo) use ($id, $companyId, $userId): void {
                $doc = $this->lockSaleDocument($pdo, $companyId, $id);
                $documentType = (string)$doc['document_type'];
                $this->requireApprovePermission($documentType);

                if ((string)$doc['status'] !== 'DRAFT') {
                    throw new RuntimeException('Solo se pueden aprobar documentos en borrador.');
                }

                $newStatus = 'APPROVED';
                $audit = ['document_number' => $doc['document_number'], 'document_type' => $documentType];

                if (SaleType::reservesOnApproval($documentType)) {
                    (new StockReservationService())->reserveSaleDocument($pdo, $companyId, (int)$doc['branch_id'], (int)$doc['warehouse_id'], (int)$doc['id'], $userId);
                    $newStatus = 'RESERVED';
                    $audit['reserved'] = 'full';
                }

                if (SaleType::reservesAvailableOnApproval($documentType)) {
                    $reserveResult = (new StockReservationService())->reserveAvailableSaleDocument($pdo, $companyId, (int)$doc['branch_id'], (int)$doc['warehouse_id'], (int)$doc['id'], $userId);
                    $audit['reserved_quantity'] = $reserveResult['reserved'];
                    $audit['pending_reservation_quantity'] = $reserveResult['pending'];
                }

                if (SaleType::directStockOnApproval($documentType)) {
                    $this->deliverVentaMostrador($pdo, $doc, $userId);
                    $newStatus = 'DELIVERED';
                }

                if (SaleType::generatesDebt($documentType)) {
                    $this->createSaleAccountDebit($pdo, $doc, $userId);
                }

                $stmt = $pdo->prepare('UPDATE sale_documents SET status = :status, approved_by = :approved_by, approved_at = NOW() WHERE id = :id');
                $stmt->execute(['status' => $newStatus, 'approved_by' => $userId, 'id' => $id]);

                $audit['status'] = $newStatus;
                (new AuditService())->log('ventas', 'aprobar', 'sale_documents', $id, null, $audit);
            });

            Session::flash('success', 'Documento aprobado correctamente.');
        } catch (Throwable $e) {
            Session::flash('error', $this->errorMessage($e));
        }

        $this->redirect('/sales-documents/show?id=' . $id);
    }

    public function convert(): void
    {
        $this->csrf();
        Permission::require('ventas.documentos.convertir');
        $companyId = (int)Auth::companyId();
        $userId = (int)Auth::id();
        $sourceId = (int)$this->input('id', 0);
        $targetType = strtoupper($this->stringInput('target_type'));

        if (!SaleType::canBeTargetFromBudget($targetType)) {
            Session::flash('error', 'Destino de conversión inválido.');
            $this->redirect('/sales-documents/show?id=' . $sourceId);
        }
        $this->requireCreatePermission($targetType);
        $this->requireApprovePermission($targetType);

        try {
            $newId = Database::transaction(function (PDO $pdo) use ($companyId, $userId, $sourceId, $targetType): int {
                $source = $this->lockSaleDocument($pdo, $companyId, $sourceId);
                if (!in_array((string)$source['document_type'], [SaleType::PRESUPUESTO, SaleType::LEGACY_PROFORMA_RESERVA], true)) {
                    throw new RuntimeException('Solo se puede convertir un presupuesto.');
                }
                if (!in_array((string)$source['status'], ['DRAFT', 'RESERVED'], true)) {
                    throw new RuntimeException('El presupuesto no está disponible para convertir.');
                }

                if ((string)$source['status'] === 'RESERVED') {
                    (new StockReservationService())->releaseSaleDocument($pdo, $companyId, (int)$source['branch_id'], (int)$source['warehouse_id'], (int)$source['id']);
                }

                $number = (new DocumentNumberService())->nextUsingPdo($pdo, $companyId, (int)$source['branch_id'], SaleType::code($targetType), SaleType::prefix($targetType));
                $newId = $this->insertSaleDocument(
                    $pdo,
                    $companyId,
                    (int)$source['branch_id'],
                    (int)$source['warehouse_id'],
                    (int)$source['customer_id'],
                    (int)($source['price_list_id'] ?? 0) ?: null,
                    $targetType,
                    $number,
                    date('Y-m-d'),
                    (int)($source['exchange_rate_id'] ?? 0) ?: null,
                    (float)$source['exchange_rate_value'],
                    (string)$source['exchange_rate_source'],
                    [
                        'subtotal_net_usd' => (float)$source['subtotal_net_usd'],
                        'subtotal_net_ars' => (float)$source['subtotal_net_ars'],
                        'discount_ars' => (float)$source['discount_ars'],
                        'vat_ars' => (float)$source['vat_ars'],
                        'total_ars' => (float)$source['total_ars'],
                        'total_usd_reference' => (float)$source['total_usd_reference'],
                    ],
                    'Generado desde presupuesto ' . (string)$source['document_number'] . ((string)($source['notes'] ?? '') !== '' ? "\n\n" . (string)$source['notes'] : ''),
                    $userId,
                    (int)$source['id']
                );

                $items = $this->documentItemsForEdit($pdo, (int)$source['id']);
                $hasCostColumns = $this->hasSaleDocumentItemColumn($pdo, 'cost_usd_snapshot')
                    && $this->hasSaleDocumentItemColumn($pdo, 'profit_percent_snapshot')
                    && $this->hasSaleDocumentItemColumn($pdo, 'expected_margin_usd')
                    && $this->hasSaleDocumentItemColumn($pdo, 'expected_margin_percent');
                $extraColumns = $hasCostColumns ? ', cost_usd_snapshot, profit_percent_snapshot, expected_margin_usd, expected_margin_percent' : '';
                $extraValues = $hasCostColumns ? ', :cost_usd_snapshot, :profit_percent_snapshot, :expected_margin_usd, :expected_margin_percent' : '';
                $insert = $pdo->prepare(
                    'INSERT INTO sale_document_items
                     (sale_document_id, product_id, description, quantity, unit_price_net_usd, discount_percent,
                      unit_price_net_ars, line_net_usd, line_net_ars, vat_rate, vat_ars, line_total_ars' . $extraColumns . ')
                     VALUES
                     (:sale_document_id, :product_id, :description, :quantity, :unit_price_net_usd, :discount_percent,
                      :unit_price_net_ars, :line_net_usd, :line_net_ars, :vat_rate, :vat_ars, :line_total_ars' . $extraValues . ')'
                );
                foreach ($items as $item) {
                    $params = [
                        'sale_document_id' => $newId,
                        'product_id' => (int)$item['product_id'],
                        'description' => (string)$item['description'],
                        'quantity' => (float)$item['quantity'],
                        'unit_price_net_usd' => (float)$item['unit_price_net_usd'],
                        'discount_percent' => (float)$item['discount_percent'],
                        'unit_price_net_ars' => (float)$item['unit_price_net_ars'],
                        'line_net_usd' => (float)$item['line_net_usd'],
                        'line_net_ars' => (float)$item['line_net_ars'],
                        'vat_rate' => (float)$item['vat_rate'],
                        'vat_ars' => (float)$item['vat_ars'],
                        'line_total_ars' => (float)$item['line_total_ars'],
                    ];
                    if ($hasCostColumns) {
                        $params['cost_usd_snapshot'] = (float)($item['cost_usd_snapshot'] ?? 0);
                        $params['profit_percent_snapshot'] = (float)($item['profit_percent_snapshot'] ?? 0);
                        $params['expected_margin_usd'] = (float)($item['expected_margin_usd'] ?? 0);
                        $params['expected_margin_percent'] = (float)($item['expected_margin_percent'] ?? 0);
                    }
                    $insert->execute($params);
                }

                $targetDoc = $this->lockSaleDocument($pdo, $companyId, $newId);
                if (SaleType::reservesAvailableOnApproval($targetType)) {
                    (new StockReservationService())->reserveAvailableSaleDocument($pdo, $companyId, (int)$targetDoc['branch_id'], (int)$targetDoc['warehouse_id'], $newId, $userId);
                }
                if (SaleType::generatesDebt($targetType)) {
                    $this->createSaleAccountDebit($pdo, $targetDoc, $userId);
                }
                $pdo->prepare('UPDATE sale_documents SET status = "APPROVED", approved_by = :user_id, approved_at = NOW() WHERE id = :id')->execute(['user_id' => $userId, 'id' => $newId]);
                $pdo->prepare('UPDATE sale_documents SET status = "CLOSED" WHERE id = :id')->execute(['id' => $sourceId]);

                (new AuditService())->log('ventas', 'convertir_presupuesto', 'sale_documents', $newId, null, [
                    'source_document' => $source['document_number'],
                    'target_document' => $number,
                    'target_type' => $targetType,
                ]);

                return $newId;
            });

            Session::flash('success', 'Presupuesto convertido correctamente. Se cerró el presupuesto y se generó el nuevo documento aprobado.');
            $this->redirect('/sales-documents/show?id=' . $newId);
        } catch (Throwable $e) {
            Session::flash('error', $this->errorMessage($e));
            $this->redirect('/sales-documents/show?id=' . $sourceId);
        }
    }

    public function void(): void
    {
        $this->csrf();
        $id = (int)$this->input('id', 0);
        $companyId = (int)Auth::companyId();
        $userId = (int)Auth::id();

        try {
            Database::transaction(function (PDO $pdo) use ($id, $companyId, $userId): void {
                $doc = $this->lockSaleDocument($pdo, $companyId, $id);
                $documentType = (string)$doc['document_type'];
                $this->requireVoidPermission($documentType);

                if (in_array((string)$doc['status'], ['INVOICED', 'PARTIAL_DELIVERED', 'DELIVERED', 'CLOSED', 'VOID'], true)) {
                    throw new RuntimeException('No se puede anular un documento con estado ' . (string)$doc['status'] . '.');
                }
                if ($this->hasReceiptAllocations($pdo, (int)$doc['id'])) {
                    throw new RuntimeException('No se puede anular porque ya tiene recibos imputados. Primero revisá/anulá las imputaciones correspondientes.');
                }

                if ((float)$doc['total_usd_reference'] > 0 && SaleType::generatesDebt($documentType) && (string)$doc['status'] === 'APPROVED') {
                    $this->reverseSaleAccountDebit($pdo, $doc, $userId);
                }

                if ((string)$doc['status'] === 'RESERVED' || (float)$this->sumReservedForDocument($pdo, (int)$doc['id']) > 0.000001) {
                    (new StockReservationService())->releaseSaleDocument($pdo, $companyId, (int)$doc['branch_id'], (int)$doc['warehouse_id'], (int)$doc['id']);
                }

                $stmt = $pdo->prepare('UPDATE sale_documents SET status = "VOID" WHERE id = :id');
                $stmt->execute(['id' => $id]);

                (new AuditService())->log('ventas', 'anular', 'sale_documents', $id, null, ['document_number' => $doc['document_number']]);
            });

            Session::flash('success', 'Documento anulado correctamente.');
        } catch (Throwable $e) {
            Session::flash('error', $this->errorMessage($e));
        }

        $this->redirect('/sales-documents/show?id=' . $id);
    }

    public function productSearch(): void
    {
        if (!Auth::check()) {
            Response::json(['ok' => false, 'message' => 'No autenticado'], 401);
        }

        $companyId = (int)Auth::companyId();
        $customerId = (int)($_GET['customer_id'] ?? 0);
        $warehouseId = (int)($_GET['warehouse_id'] ?? 0);
        $q = trim((string)($_GET['q'] ?? ''));

        if ($customerId <= 0) {
            Response::json(['ok' => false, 'message' => 'Seleccioná un cliente antes de buscar productos.'], 422);
        }

        $rate = (new ExchangeRateService())->latest($companyId, 'MANUAL_VENTA');
        if (!$rate) {
            Response::json(['ok' => false, 'message' => 'No hay tipo de cambio MANUAL_VENTA cargado.'], 422);
        }
        $rateValue = (float)$rate['rate'];

        $pdo = Database::pdo();
        $pricing = new SalePricingService();
        $priceListId = $pricing->customerPriceListId($pdo, $companyId, $customerId);

        $stmt = $pdo->prepare(
            'SELECT p.id, p.code, p.name, p.category_id, p.item_type, p.manages_stock,
                    u.code AS unit_code, tr.rate AS vat_rate,
                    COALESCE(sb.quantity_on_hand, 0) AS quantity_on_hand,
                    COALESCE(sb.quantity_reserved, 0) AS quantity_reserved,
                    COALESCE(sb.quantity_available, 0) AS quantity_available
             FROM products p
             INNER JOIN units u ON u.id = p.unit_id
             INNER JOIN tax_rates tr ON tr.id = p.tax_rate_id
             LEFT JOIN stock_balances sb ON sb.product_id = p.id AND sb.warehouse_id = :warehouse_id
             WHERE p.company_id = :company_id
               AND p.is_active = 1
               AND (:q_exact = "" OR p.code LIKE :q_code OR p.name LIKE :q_name OR p.barcode LIKE :q_barcode)
             ORDER BY p.name ASC
             LIMIT 25'
        );
        $stmt->execute([
            'warehouse_id' => $warehouseId > 0 ? $warehouseId : 0,
            'company_id' => $companyId,
            'q_exact' => $q,
            'q_code' => '%' . $q . '%',
            'q_name' => '%' . $q . '%',
            'q_barcode' => '%' . $q . '%',
        ]);

        $items = [];
        $showCosts = Permission::has('productos.costos.ver');
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            try {
                $product = $pricing->productForSale($pdo, $companyId, (int)$row['id'], $priceListId);
                $discount = $pricing->discountPercent($pdo, $companyId, $customerId, (int)$row['id'], (int)$row['category_id']);
                $line = $pricing->calculateLine($product, 1.0, $rateValue, $discount);
                $items[] = [
                    'id' => (int)$row['id'],
                    'code' => (string)$row['code'],
                    'name' => (string)$row['name'],
                    'item_type' => (string)$row['item_type'],
                    'unit_code' => (string)$row['unit_code'],
                    'manages_stock' => (int)$row['manages_stock'],
                    'net_price_usd' => (float)$product['net_price_usd'],
                    'cost_usd' => $showCosts ? (float)($product['cost_usd'] ?? 0) : null,
                    'profit_percent' => $showCosts ? (float)($product['profit_percent'] ?? 0) : null,
                    'show_costs' => $showCosts,
                    'discount_percent' => $discount,
                    'vat_rate' => (float)$row['vat_rate'],
                    'unit_price_net_ars' => $line['unit_price_net_ars'],
                    'unit_price_total_ars' => round($line['unit_price_net_ars'] * (1 + ((float)$row['vat_rate'] / 100)), 4),
                    'quantity_on_hand' => (float)$row['quantity_on_hand'],
                    'quantity_reserved' => (float)$row['quantity_reserved'],
                    'quantity_available' => (float)$row['quantity_available'],
                    'exchange_rate' => $rateValue,
                ];
            } catch (RuntimeException) {
                continue;
            }
        }

        Response::json(['ok' => true, 'items' => $items, 'exchange_rate' => $rateValue]);
    }

    private function formData(string $title, array $rate, int $selectedCustomerId = 0): array
    {
        $companyId = (int)Auth::companyId();
        $branchId = (int)Auth::branchId();
        $pdo = Database::pdo();

        $customers = $pdo->prepare('SELECT id, business_name, cuit, fiscal_condition FROM customers WHERE company_id = :company_id AND is_active = 1 ORDER BY business_name ASC LIMIT 1000');
        $customers->execute(['company_id' => $companyId]);

        $warehouses = $pdo->prepare('SELECT id, name FROM warehouses WHERE company_id = :company_id AND branch_id = :branch_id AND is_active = 1 ORDER BY is_default DESC, name ASC');
        $warehouses->execute(['company_id' => $companyId, 'branch_id' => $branchId]);

        return [
            'title' => $title,
            'mode' => 'create',
            'customers' => $customers->fetchAll(PDO::FETCH_ASSOC),
            'warehouses' => $warehouses->fetchAll(PDO::FETCH_ASSOC),
            'rate' => $rate,
            'today' => date('Y-m-d'),
            'selectedCustomerId' => $selectedCustomerId,
            'documentTypes' => SaleType::activeLabels(),
            'typeHelp' => array_reduce(SaleType::activeTypes(), static function (array $carry, string $type): array { $carry[$type] = SaleType::help($type); return $carry; }, []),
        ];
    }

    private function buildLines(PDO $pdo, int $companyId, int $customerId, ?int $priceListId, array $productIds, array $quantities, array $discounts, float $rateValue): array
    {
        $pricing = new SalePricingService();
        $lines = [];
        $seenCount = min(count($productIds), count($quantities));

        for ($i = 0; $i < $seenCount; $i++) {
            $productId = (int)$productIds[$i];
            $quantity = $this->dec((string)$quantities[$i]);
            if ($productId <= 0 || $quantity <= 0) {
                continue;
            }

            $product = $pricing->productForSale($pdo, $companyId, $productId, $priceListId);
            $defaultDiscount = $pricing->discountPercent($pdo, $companyId, $customerId, $productId, (int)$product['category_id']);
            $discount = array_key_exists($i, $discounts) && trim((string)$discounts[$i]) !== '' ? $this->dec((string)$discounts[$i]) : $defaultDiscount;
            $discount = max(0.0, min(100.0, $discount));
            $calc = $pricing->calculateLine($product, $quantity, $rateValue, $discount);

            $lines[] = [
                'product_id' => $productId,
                'description' => (string)$product['name'],
                'quantity' => $quantity,
                'discount_percent' => $discount,
                'manages_stock' => (int)$product['manages_stock'],
            ] + $calc;
        }

        return $lines;
    }

    private function totals(array $lines, float $saleExchangeRate, float $accountExchangeRate, float $documentDiscountPercent, float $documentDiscountAmount): array
    {
        $itemDiscountArs = 0.0;
        $lineNetUsd = 0.0;
        $lineNetArs = 0.0;
        $lineVatArs = 0.0;
        $lineTotalArs = 0.0;

        foreach ($lines as $line) {
            $itemDiscountArs += (float)$line['discount_ars'];
            $lineNetUsd += (float)$line['line_net_usd'];
            $lineNetArs += (float)$line['line_net_ars'];
            $lineVatArs += (float)$line['vat_ars'];
            $lineTotalArs += (float)$line['line_total_ars'];
        }

        $subtotalForDiscount = max(0.0, round($lineNetArs, 4));
        $documentDiscount = $documentDiscountAmount > 0
            ? min($documentDiscountAmount, $subtotalForDiscount)
            : min(round($subtotalForDiscount * (max(0.0, min(100.0, $documentDiscountPercent)) / 100), 4), $subtotalForDiscount);

        $discountRatio = $subtotalForDiscount > 0 ? $documentDiscount / $subtotalForDiscount : 0.0;
        $vatAfterDocumentDiscount = round($lineVatArs * (1 - $discountRatio), 4);
        $netAfterDocumentDiscount = round($subtotalForDiscount - $documentDiscount, 4);
        $totalArs = round($netAfterDocumentDiscount + $vatAfterDocumentDiscount, 4);
        $totalUsdReference = $accountExchangeRate > 0 ? round($totalArs / $accountExchangeRate, 4) : 0.0;

        return [
            'subtotal_net_usd' => round($lineNetUsd * (1 - $discountRatio), 4),
            'subtotal_net_ars' => $netAfterDocumentDiscount,
            'discount_ars' => round($itemDiscountArs + $documentDiscount, 4),
            'vat_ars' => $vatAfterDocumentDiscount,
            'total_ars' => $totalArs,
            'total_usd_reference' => $totalUsdReference,
            'document_discount_percent' => $documentDiscountAmount > 0 ? 0.0 : round(max(0.0, min(100.0, $documentDiscountPercent)), 4),
            'document_discount_ars' => round($documentDiscount, 4),
            'document_discount_ratio' => round($discountRatio, 8),
        ];
    }

    /** @param array<int,array<string,mixed>> $lines */
    private function applyDocumentDiscountToMargins(array $lines, array $totals): array
    {
        $ratio = max(0.0, min(1.0, (float)($totals['document_discount_ratio'] ?? 0)));
        foreach ($lines as &$line) {
            $quantity = (float)($line['quantity'] ?? 0);
            $costUsd = (float)($line['cost_usd_snapshot'] ?? 0);
            $costTotalUsd = round($costUsd * $quantity, 4);
            $lineNetUsdAfterDocumentDiscount = round((float)($line['line_net_usd'] ?? 0) * (1 - $ratio), 4);
            $margin = round($lineNetUsdAfterDocumentDiscount - $costTotalUsd, 4);
            $line['expected_margin_usd'] = $margin;
            $line['expected_margin_percent'] = $costTotalUsd > 0 ? round(($margin / $costTotalUsd) * 100, 4) : 0.0;
        }
        unset($line);
        return $lines;
    }

    private function insertSaleDocument(PDO $pdo, int $companyId, int $branchId, int $warehouseId, int $customerId, ?int $priceListId, string $documentType, string $number, string $issueDate, ?int $exchangeRateId, float $rateValue, string $rateSource, ?int $accountExchangeRateId, float $accountRateValue, string $accountRateSource, array $totals, string $notes, int $userId, ?int $sourceSaleDocumentId): int
    {
        $hasSource = $this->hasSaleDocumentColumn($pdo, 'source_sale_document_id');
        $hasDocumentDiscountColumns = $this->hasSaleDocumentColumn($pdo, 'document_discount_percent') && $this->hasSaleDocumentColumn($pdo, 'document_discount_ars');
        $hasAccountRateColumns = $this->hasSaleDocumentColumn($pdo, 'account_exchange_rate_value');
        $sourceColumn = $hasSource ? ', source_sale_document_id' : '';
        $sourceValue = $hasSource ? ', :source_sale_document_id' : '';
        $discountColumns = $hasDocumentDiscountColumns ? ', document_discount_percent, document_discount_ars' : '';
        $discountValues = $hasDocumentDiscountColumns ? ', :document_discount_percent, :document_discount_ars' : '';
        $accountRateColumns = $hasAccountRateColumns ? ', account_exchange_rate_id, account_exchange_rate_value, account_exchange_rate_source' : '';
        $accountRateValues = $hasAccountRateColumns ? ', :account_exchange_rate_id, :account_exchange_rate_value, :account_exchange_rate_source' : '';
        $stmt = $pdo->prepare(
            'INSERT INTO sale_documents
             (company_id, branch_id, warehouse_id, customer_id' . $sourceColumn . ', price_list_id, document_type, document_number, issue_date,
              exchange_rate_id, exchange_rate_value, exchange_rate_source, status, affects_stock_reservation, can_invoice_without_stock,
              subtotal_net_usd, subtotal_net_ars, discount_ars, vat_ars, total_ars, total_usd_reference' . $accountRateColumns . $discountColumns . ', notes, created_by)
             VALUES
             (:company_id, :branch_id, :warehouse_id, :customer_id' . $sourceValue . ', :price_list_id, :document_type, :document_number, :issue_date,
              :exchange_rate_id, :exchange_rate_value, :exchange_rate_source, "DRAFT", :affects_stock_reservation, :can_invoice_without_stock,
              :subtotal_net_usd, :subtotal_net_ars, :discount_ars, :vat_ars, :total_ars, :total_usd_reference' . $accountRateValues . $discountValues . ', :notes, :created_by)'
        );
        $params = [
            'company_id' => $companyId,
            'branch_id' => $branchId,
            'warehouse_id' => $warehouseId,
            'customer_id' => $customerId,
            'price_list_id' => $priceListId,
            'document_type' => $documentType,
            'document_number' => $number,
            'issue_date' => $issueDate,
            'exchange_rate_id' => $exchangeRateId,
            'exchange_rate_value' => $rateValue,
            'exchange_rate_source' => $rateSource,
            'affects_stock_reservation' => SaleType::reservesOnApproval($documentType) || SaleType::reservesAvailableOnApproval($documentType) ? 1 : 0,
            'can_invoice_without_stock' => $documentType === SaleType::VENTA_ANTICIPADA || $documentType === SaleType::PROFORMA_X ? 1 : 0,
            'subtotal_net_usd' => $totals['subtotal_net_usd'],
            'subtotal_net_ars' => $totals['subtotal_net_ars'],
            'discount_ars' => $totals['discount_ars'],
            'vat_ars' => $totals['vat_ars'],
            'total_ars' => $totals['total_ars'],
            'total_usd_reference' => $totals['total_usd_reference'],
            'notes' => $notes !== '' ? $notes : null,
            'created_by' => $userId,
        ];
        if ($hasAccountRateColumns) {
            $params['account_exchange_rate_id'] = $accountExchangeRateId;
            $params['account_exchange_rate_value'] = $accountRateValue;
            $params['account_exchange_rate_source'] = $accountRateSource;
        }
        if ($hasSource) {
            $params['source_sale_document_id'] = $sourceSaleDocumentId;
        }
        if ($hasDocumentDiscountColumns) {
            $params['document_discount_percent'] = $totals['document_discount_percent'] ?? 0;
            $params['document_discount_ars'] = $totals['document_discount_ars'] ?? 0;
        }
        $stmt->execute($params);
        return (int)$pdo->lastInsertId();
    }

    private function updateSaleDocument(PDO $pdo, int $saleDocumentId, int $warehouseId, int $customerId, ?int $priceListId, string $documentType, string $issueDate, ?int $exchangeRateId, float $rateValue, string $rateSource, ?int $accountExchangeRateId, float $accountRateValue, string $accountRateSource, array $totals, string $notes): void
    {
        $discountSet = ($this->hasSaleDocumentColumn($pdo, 'document_discount_percent') && $this->hasSaleDocumentColumn($pdo, 'document_discount_ars'))
            ? ', document_discount_percent = :document_discount_percent, document_discount_ars = :document_discount_ars'
            : '';
        $accountRateSet = $this->hasSaleDocumentColumn($pdo, 'account_exchange_rate_value')
            ? ', account_exchange_rate_id = :account_exchange_rate_id, account_exchange_rate_value = :account_exchange_rate_value, account_exchange_rate_source = :account_exchange_rate_source'
            : '';
        $stmt = $pdo->prepare(
            'UPDATE sale_documents
             SET warehouse_id = :warehouse_id,
                 customer_id = :customer_id,
                 price_list_id = :price_list_id,
                 document_type = :document_type,
                 issue_date = :issue_date,
                 exchange_rate_id = :exchange_rate_id,
                 exchange_rate_value = :exchange_rate_value,
                 exchange_rate_source = :exchange_rate_source' . $accountRateSet . ',
                 affects_stock_reservation = :affects_stock_reservation,
                 can_invoice_without_stock = :can_invoice_without_stock,
                 subtotal_net_usd = :subtotal_net_usd,
                 subtotal_net_ars = :subtotal_net_ars,
                 discount_ars = :discount_ars,
                 vat_ars = :vat_ars,
                 total_ars = :total_ars,
                 total_usd_reference = :total_usd_reference' . $discountSet . ',
                 notes = :notes
             WHERE id = :id'
        );
        $params = [
            'warehouse_id' => $warehouseId,
            'customer_id' => $customerId,
            'price_list_id' => $priceListId,
            'document_type' => $documentType,
            'issue_date' => $issueDate,
            'exchange_rate_id' => $exchangeRateId,
            'exchange_rate_value' => $rateValue,
            'exchange_rate_source' => $rateSource,
            'affects_stock_reservation' => SaleType::reservesOnApproval($documentType) || SaleType::reservesAvailableOnApproval($documentType) ? 1 : 0,
            'can_invoice_without_stock' => $documentType === SaleType::VENTA_ANTICIPADA || $documentType === SaleType::PROFORMA_X ? 1 : 0,
            'subtotal_net_usd' => $totals['subtotal_net_usd'],
            'subtotal_net_ars' => $totals['subtotal_net_ars'],
            'discount_ars' => $totals['discount_ars'],
            'vat_ars' => $totals['vat_ars'],
            'total_ars' => $totals['total_ars'],
            'total_usd_reference' => $totals['total_usd_reference'],
            'notes' => $notes !== '' ? $notes : null,
            'id' => $saleDocumentId,
        ];
        if ($accountRateSet !== '') {
            $params['account_exchange_rate_id'] = $accountExchangeRateId;
            $params['account_exchange_rate_value'] = $accountRateValue;
            $params['account_exchange_rate_source'] = $accountRateSource;
        }
        if ($discountSet !== '') {
            $params['document_discount_percent'] = $totals['document_discount_percent'] ?? 0;
            $params['document_discount_ars'] = $totals['document_discount_ars'] ?? 0;
        }
        $stmt->execute($params);
    }

    private function insertItems(PDO $pdo, int $saleDocumentId, array $lines): void
    {
        $hasCostColumns = $this->hasSaleDocumentItemColumn($pdo, 'cost_usd_snapshot')
            && $this->hasSaleDocumentItemColumn($pdo, 'profit_percent_snapshot')
            && $this->hasSaleDocumentItemColumn($pdo, 'expected_margin_usd')
            && $this->hasSaleDocumentItemColumn($pdo, 'expected_margin_percent');

        $extraColumns = $hasCostColumns ? ', cost_usd_snapshot, profit_percent_snapshot, expected_margin_usd, expected_margin_percent' : '';
        $extraValues = $hasCostColumns ? ', :cost_usd_snapshot, :profit_percent_snapshot, :expected_margin_usd, :expected_margin_percent' : '';

        $itemStmt = $pdo->prepare(
            'INSERT INTO sale_document_items
             (sale_document_id, product_id, description, quantity, unit_price_net_usd, discount_percent,
              unit_price_net_ars, line_net_usd, line_net_ars, vat_rate, vat_ars, line_total_ars' . $extraColumns . ')
             VALUES
             (:sale_document_id, :product_id, :description, :quantity, :unit_price_net_usd, :discount_percent,
              :unit_price_net_ars, :line_net_usd, :line_net_ars, :vat_rate, :vat_ars, :line_total_ars' . $extraValues . ')'
        );

        foreach ($lines as $line) {
            $params = [
                'sale_document_id' => $saleDocumentId,
                'product_id' => $line['product_id'],
                'description' => $line['description'],
                'quantity' => $line['quantity'],
                'unit_price_net_usd' => $line['unit_price_net_usd'],
                'discount_percent' => $line['discount_percent'],
                'unit_price_net_ars' => $line['unit_price_net_ars'],
                'line_net_usd' => $line['line_net_usd'],
                'line_net_ars' => $line['line_net_ars'],
                'vat_rate' => $line['vat_rate'],
                'vat_ars' => $line['vat_ars'],
                'line_total_ars' => $line['line_total_ars'],
            ];
            if ($hasCostColumns) {
                $params['cost_usd_snapshot'] = $line['cost_usd_snapshot'] ?? 0;
                $params['profit_percent_snapshot'] = $line['profit_percent_snapshot'] ?? 0;
                $params['expected_margin_usd'] = $line['expected_margin_usd'] ?? 0;
                $params['expected_margin_percent'] = $line['expected_margin_percent'] ?? 0;
            }
            $itemStmt->execute($params);
        }
    }

    private function deliverVentaMostrador(PDO $pdo, array $doc, int $userId): void
    {
        $items = $this->documentItemsForEdit($pdo, (int)$doc['id']);
        $stockService = new StockMovementService();
        foreach ($items as $item) {
            $quantity = round((float)$item['quantity'], 6);
            if ($quantity <= 0) {
                continue;
            }
            if ((int)$item['manages_stock'] === 1) {
                $stockService->decreaseStock($pdo, [
                    'company_id' => (int)$doc['company_id'],
                    'branch_id' => (int)$doc['branch_id'],
                    'warehouse_id' => (int)$doc['warehouse_id'],
                    'product_id' => (int)$item['product_id'],
                    'source_type' => 'VENTA_MOSTRADOR',
                    'source_id' => (int)$doc['id'],
                    'notes' => 'Venta mostrador ' . (string)$doc['document_number'],
                    'created_by' => $userId,
                ], $quantity, 'OUT');
            }
            $pdo->prepare('UPDATE sale_document_items SET quantity_delivered = :quantity_delivered WHERE id = :id')->execute([
                'quantity_delivered' => $quantity,
                'id' => (int)$item['id'],
            ]);
        }
    }

    private function assertStockAvailableForLines(PDO $pdo, int $companyId, int $branchId, int $warehouseId, array $lines, string $prefixMessage): void
    {
        foreach ($lines as $line) {
            if ((int)($line['manages_stock'] ?? 0) !== 1) {
                continue;
            }
            $stmt = $pdo->prepare(
                'SELECT COALESCE(quantity_available, 0)
                 FROM stock_balances
                 WHERE company_id = :company_id AND branch_id = :branch_id AND warehouse_id = :warehouse_id AND product_id = :product_id
                 FOR UPDATE'
            );
            $stmt->execute([
                'company_id' => $companyId,
                'branch_id' => $branchId,
                'warehouse_id' => $warehouseId,
                'product_id' => (int)$line['product_id'],
            ]);
            $available = (float)($stmt->fetchColumn() ?: 0);
            if ($available + 0.000001 < (float)$line['quantity']) {
                throw new RuntimeException($prefixMessage . ' Producto: ' . (string)$line['description'] . '. Disponible: ' . number_format($available, 3, ',', '.') . '.');
            }
        }
    }

    private function lockCustomer(PDO $pdo, int $companyId, int $customerId): array
    {
        $stmt = $pdo->prepare('SELECT * FROM customers WHERE company_id = :company_id AND id = :id AND is_active = 1 LIMIT 1');
        $stmt->execute(['company_id' => $companyId, 'id' => $customerId]);
        $customer = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$customer) {
            throw new RuntimeException('Cliente inválido o inactivo.');
        }
        return $customer;
    }

    private function validateWarehouse(PDO $pdo, int $companyId, int $branchId, int $warehouseId): void
    {
        $stmt = $pdo->prepare('SELECT id FROM warehouses WHERE company_id = :company_id AND branch_id = :branch_id AND id = :id AND is_active = 1 LIMIT 1');
        $stmt->execute(['company_id' => $companyId, 'branch_id' => $branchId, 'id' => $warehouseId]);
        if (!$stmt->fetch()) {
            throw new RuntimeException('Depósito inválido para la sucursal actual.');
        }
    }

    private function loadDocumentForView(int $id): array
    {
        $companyId = (int)Auth::companyId();
        $pdo = Database::pdo();
        $sourceJoin = $this->hasSaleDocumentColumn($pdo, 'source_sale_document_id')
            ? 'LEFT JOIN sale_documents src ON src.id = sd.source_sale_document_id'
            : 'LEFT JOIN sale_documents src ON 1 = 0';
        $sourceSelect = $this->hasSaleDocumentColumn($pdo, 'source_sale_document_id')
            ? 'sd.source_sale_document_id,'
            : 'NULL AS source_sale_document_id,';

        $stmt = $pdo->prepare(
            'SELECT sd.*, ' . $sourceSelect . ' c.business_name AS customer_name, c.trade_name AS customer_trade_name, c.cuit AS customer_cuit, c.fiscal_condition AS customer_fiscal_condition,
                    c.email AS customer_email, c.phone AS customer_phone, c.whatsapp AS customer_whatsapp, c.address AS customer_address, c.city AS customer_city, c.province AS customer_province, c.country AS customer_country,
                    cc.name AS customer_commercial_condition, b.name AS branch_name, w.name AS warehouse_name,
                    u.full_name AS approved_by_name,
                    src.document_number AS source_document_number, src.document_type AS source_document_type
             FROM sale_documents sd
             INNER JOIN customers c ON c.id = sd.customer_id
             LEFT JOIN commercial_conditions cc ON cc.id = c.commercial_condition_id
             INNER JOIN branches b ON b.id = sd.branch_id
             LEFT JOIN warehouses w ON w.id = sd.warehouse_id
             LEFT JOIN users u ON u.id = sd.approved_by
             ' . $sourceJoin . '
             WHERE sd.company_id = :company_id AND sd.id = :id
             LIMIT 1'
        );
        $stmt->execute(['company_id' => $companyId, 'id' => $id]);
        $document = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$document) {
            http_response_code(404);
            $this->view('errors/404', ['title' => 'Documento no encontrado']);
            exit;
        }

        $items = $this->documentItemsForView($pdo, $id);

        $movements = $pdo->prepare('SELECT * FROM customer_account_movements WHERE sale_document_id = :id ORDER BY id ASC');
        $movements->execute(['id' => $id]);
        $movementRows = $movements->fetchAll(PDO::FETCH_ASSOC);

        $accountRate = $this->effectiveCustomerAccountRate($pdo, $document, $movementRows);
        if ($accountRate['rate'] > 0) {
            // Importante: el comprobante entregable al cliente debe mostrar la cotización usada para cuenta corriente
            // (BNA comprador), no la cotización interna MANUAL_VENTA usada solo para formar precios del POS.
            $document['account_exchange_rate_value'] = $accountRate['rate'];
            $document['account_exchange_rate_source'] = $accountRate['source'];
            $document['account_exchange_rate_id'] = $accountRate['id'];
            $document['total_usd_reference'] = round(((float)$document['total_ars']) / $accountRate['rate'], 4);
        }

        $receiptAllocations = $this->saleDocumentReceiptAllocations($pdo, $id);
        $allocatedUsd = 0.0;
        foreach ($receiptAllocations as $allocation) {
            $allocatedUsd += (float)$allocation['amount_usd'];
        }
        $outstandingUsd = max(0.0, round((float)$document['total_usd_reference'] - $allocatedUsd, 4));

        return [
            'document' => $document,
            'items' => $items,
            'accountMovements' => $movementRows,
            'receiptAllocations' => $receiptAllocations,
            'deliveryNotes' => $this->saleDocumentDeliveryNotes($pdo, $id),
            'customerReturns' => $this->saleDocumentReturns($pdo, $id),
            'unappliedReceipts' => $this->customerUnappliedReceipts($pdo, (int)$document['company_id'], (int)$document['customer_id']),
            'outstandingUsd' => $outstandingUsd,
            'sourceDocument' => !empty($document['source_sale_document_id']) ? ['id' => (int)$document['source_sale_document_id'], 'number' => (string)$document['source_document_number'], 'type' => (string)$document['source_document_type']] : null,
            'generatedDocuments' => $this->generatedDocuments($pdo, $id),
        ];
    }


    /**
     * Devuelve la cotización que corresponde mostrar e imputar en cuenta corriente.
     * Nunca debe devolver MANUAL_VENTA para comprobantes de cliente: MANUAL_VENTA es solo interna del POS.
     *
     * @param array<string,mixed> $document
     * @param array<int,array<string,mixed>> $movementRows
     * @return array{id:?int, rate:float, source:string}
     */
    private function effectiveCustomerAccountRate(PDO $pdo, array $document, array $movementRows = []): array
    {
        $docRate = (float)($document['account_exchange_rate_value'] ?? 0);
        $docSource = strtoupper((string)($document['account_exchange_rate_source'] ?? ''));
        if ($docRate > 0 && str_contains($docSource, 'BNA_BILLETE_COMPRADOR')) {
            return [
                'id' => isset($document['account_exchange_rate_id']) ? (int)$document['account_exchange_rate_id'] : null,
                'rate' => $docRate,
                'source' => 'BNA_BILLETE_COMPRADOR',
            ];
        }

        foreach ($movementRows as $movement) {
            if (($movement['movement_type'] ?? '') === 'DEBIT' && (float)($movement['exchange_rate_value'] ?? 0) > 0) {
                $movementRate = (float)$movement['exchange_rate_value'];
                $saleRate = (float)($document['exchange_rate_value'] ?? 0);
                // Si el movimiento tiene la misma cotización de venta manual, no lo usamos como BNA.
                if ($saleRate <= 0 || abs($movementRate - $saleRate) > 0.000001) {
                    return ['id' => null, 'rate' => $movementRate, 'source' => 'BNA_BILLETE_COMPRADOR'];
                }
            }
        }

        $rate = $this->latestBnaBuyerRateForDocument($pdo, (int)$document['company_id'], (string)($document['issue_date'] ?? date('Y-m-d')));
        if ($rate !== null) {
            return $rate;
        }

        return [
            'id' => isset($document['account_exchange_rate_id']) ? (int)$document['account_exchange_rate_id'] : null,
            'rate' => $docRate > 0 ? $docRate : (float)($document['exchange_rate_value'] ?? 0),
            'source' => $docRate > 0 ? (string)($document['account_exchange_rate_source'] ?? 'BNA_BILLETE_COMPRADOR') : 'SIN_BNA_CONFIGURADO',
        ];
    }

    /** @return array{id:?int, rate:float, source:string}|null */
    private function latestBnaBuyerRateForDocument(PDO $pdo, int $companyId, string $issueDate): ?array
    {
        $stmt = $pdo->prepare(
            'SELECT er.id, er.rate
             FROM exchange_rates er
             INNER JOIN exchange_rate_types ert ON ert.id = er.exchange_rate_type_id
             WHERE er.company_id = :company_id
               AND ert.code = "BNA_BILLETE_COMPRADOR"
               AND er.rate_date <= :issue_date
             ORDER BY er.rate_date DESC, er.rate_time DESC, er.id DESC
             LIMIT 1'
        );
        $stmt->execute(['company_id' => $companyId, 'issue_date' => $issueDate]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            $stmt = $pdo->prepare(
                'SELECT er.id, er.rate
                 FROM exchange_rates er
                 INNER JOIN exchange_rate_types ert ON ert.id = er.exchange_rate_type_id
                 WHERE er.company_id = :company_id
                   AND ert.code = "BNA_BILLETE_COMPRADOR"
                 ORDER BY er.rate_date DESC, er.rate_time DESC, er.id DESC
                 LIMIT 1'
            );
            $stmt->execute(['company_id' => $companyId]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
        }
        if (!$row) {
            return null;
        }
        return ['id' => (int)$row['id'], 'rate' => (float)$row['rate'], 'source' => 'BNA_BILLETE_COMPRADOR'];
    }

    private function documentItemsForView(PDO $pdo, int $saleDocumentId): array
    {
        $stmt = $pdo->prepare(
            'SELECT sdi.*, p.code AS product_code, p.manages_stock, u.code AS unit_code
             FROM sale_document_items sdi
             INNER JOIN products p ON p.id = sdi.product_id
             INNER JOIN units u ON u.id = p.unit_id
             WHERE sdi.sale_document_id = :id
             ORDER BY sdi.id ASC'
        );
        $stmt->execute(['id' => $saleDocumentId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function documentItemsForEdit(PDO $pdo, int $saleDocumentId): array
    {
        $stmt = $pdo->prepare(
            'SELECT sdi.*, p.code AS product_code, p.name AS product_name, p.manages_stock, u.code AS unit_code,
                    COALESCE(sb.quantity_available, 0) AS quantity_available
             FROM sale_document_items sdi
             INNER JOIN sale_documents sd ON sd.id = sdi.sale_document_id
             INNER JOIN products p ON p.id = sdi.product_id
             INNER JOIN units u ON u.id = p.unit_id
             LEFT JOIN stock_balances sb ON sb.product_id = p.id AND sb.warehouse_id = sd.warehouse_id
             WHERE sdi.sale_document_id = :id
             ORDER BY sdi.id ASC'
        );
        $stmt->execute(['id' => $saleDocumentId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function generatedDocuments(PDO $pdo, int $sourceId): array
    {
        if (!$this->hasSaleDocumentColumn($pdo, 'source_sale_document_id')) {
            return [];
        }
        $stmt = $pdo->prepare(
            'SELECT id, document_number, document_type, issue_date, status, total_ars, total_usd_reference
             FROM sale_documents
             WHERE source_sale_document_id = :id
             ORDER BY id DESC'
        );
        $stmt->execute(['id' => $sourceId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function saleDocumentReturns(PDO $pdo, int $saleDocumentId): array
    {
        if (!$this->tableExists($pdo, 'customer_return_documents')) {
            return [];
        }
        $stmt = $pdo->prepare(
            'SELECT id, document_number, return_type, issue_date, status, credit_usd, credit_ars
             FROM customer_return_documents
             WHERE sale_document_id = :id
             ORDER BY issue_date DESC, id DESC'
        );
        $stmt->execute(['id' => $saleDocumentId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function saleDocumentDeliveryNotes(PDO $pdo, int $saleDocumentId): array
    {
        $stmt = $pdo->prepare(
            'SELECT cdn.*, w.name AS warehouse_name,
                    COALESCE((SELECT COUNT(*) FROM customer_delivery_note_items i WHERE i.customer_delivery_note_id = cdn.id), 0) AS item_count,
                    COALESCE((SELECT SUM(i.quantity) FROM customer_delivery_note_items i WHERE i.customer_delivery_note_id = cdn.id), 0) AS total_quantity
             FROM customer_delivery_notes cdn
             INNER JOIN warehouses w ON w.id = cdn.warehouse_id
             WHERE cdn.sale_document_id = :id
             ORDER BY cdn.issue_date DESC, cdn.id DESC'
        );
        $stmt->execute(['id' => $saleDocumentId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function customerUnappliedReceipts(PDO $pdo, int $companyId, int $customerId): array
    {
        $stmt = $pdo->prepare(
            'SELECT r.id, r.receipt_number, r.issue_date, r.status, r.exchange_rate_value, r.unapplied_usd, r.unapplied_ars,
                    GROUP_CONCAT(CONCAT(pm.name, " ", p.currency_code, " ", CAST(p.amount AS CHAR), IF(p.reference IS NULL OR p.reference = "", "", CONCAT(" ref ", p.reference))) ORDER BY p.id SEPARATOR " | ") AS payment_summary
             FROM receipts r
             LEFT JOIN payments p ON p.receipt_id = r.id AND p.status <> "VOID"
             LEFT JOIN payment_methods pm ON pm.id = p.payment_method_id
             WHERE r.company_id = :company_id
               AND r.customer_id = :customer_id
               AND r.status <> "VOID"
               AND r.unapplied_usd > 0.0001
             GROUP BY r.id, r.receipt_number, r.issue_date, r.status, r.exchange_rate_value, r.unapplied_usd, r.unapplied_ars
             ORDER BY r.issue_date ASC, r.id ASC
             LIMIT 100'
        );
        $stmt->execute(['company_id' => $companyId, 'customer_id' => $customerId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function saleDocumentReceiptAllocations(PDO $pdo, int $saleDocumentId): array
    {
        $stmt = $pdo->prepare(
            'SELECT pa.id AS allocation_id, pa.amount_usd, pa.amount_ars, pa.exchange_rate_value, pa.exchange_difference_ars, pa.allocation_date,
                    r.id AS receipt_id, r.receipt_number, r.issue_date AS receipt_date, r.status AS receipt_status,
                    GROUP_CONCAT(CONCAT(pm.name, " ", p.currency_code, " ", CAST(p.amount AS CHAR), IF(p.reference IS NULL OR p.reference = "", "", CONCAT(" ref ", p.reference))) ORDER BY p.id SEPARATOR " | ") AS payment_summary
             FROM payment_allocations pa
             INNER JOIN receipts r ON r.id = pa.receipt_id
             LEFT JOIN payments p ON p.receipt_id = r.id AND p.status <> "VOID"
             LEFT JOIN payment_methods pm ON pm.id = p.payment_method_id
             WHERE pa.sale_document_id = :id
               AND r.status <> "VOID"
             GROUP BY pa.id, pa.amount_usd, pa.amount_ars, pa.exchange_rate_value, pa.exchange_difference_ars, pa.allocation_date, r.id, r.receipt_number, r.issue_date, r.status
             ORDER BY r.issue_date ASC, r.id ASC'
        );
        $stmt->execute(['id' => $saleDocumentId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function loadDocument(PDO $pdo, int $companyId, int $id): ?array
    {
        $stmt = $pdo->prepare('SELECT * FROM sale_documents WHERE company_id = :company_id AND id = :id LIMIT 1');
        $stmt->execute(['company_id' => $companyId, 'id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    private function lockSaleDocument(PDO $pdo, int $companyId, int $id): array
    {
        $stmt = $pdo->prepare('SELECT * FROM sale_documents WHERE company_id = :company_id AND id = :id FOR UPDATE');
        $stmt->execute(['company_id' => $companyId, 'id' => $id]);
        $doc = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$doc) {
            throw new RuntimeException('Documento no encontrado.');
        }
        return $doc;
    }

    private function createSaleAccountDebit(PDO $pdo, array $doc, int $userId): void
    {
        if ((float)$doc['total_usd_reference'] <= 0) {
            return;
        }
        $description = SaleType::label((string)$doc['document_type']) . ' ' . (string)$doc['document_number'];
        $stmt = $pdo->prepare(
            'INSERT INTO customer_account_movements
             (company_id, customer_id, branch_id, movement_date, movement_type, account_currency_code,
              amount_usd, amount_ars, exchange_rate_value, sale_document_id, description, created_by)
             VALUES
             (:company_id, :customer_id, :branch_id, NOW(), "DEBIT", "USD",
              :amount_usd, :amount_ars, :exchange_rate_value, :sale_document_id, :description, :created_by)'
        );
        $stmt->execute([
            'company_id' => (int)$doc['company_id'],
            'customer_id' => (int)$doc['customer_id'],
            'branch_id' => (int)$doc['branch_id'],
            'amount_usd' => (float)$doc['total_usd_reference'],
            'amount_ars' => (float)$doc['total_ars'],
            'exchange_rate_value' => (float)($doc['account_exchange_rate_value'] ?? $doc['exchange_rate_value']),
            'sale_document_id' => (int)$doc['id'],
            'description' => $description,
            'created_by' => $userId,
        ]);
    }

    private function reverseSaleAccountDebit(PDO $pdo, array $doc, int $userId): void
    {
        $stmt = $pdo->prepare(
            'INSERT INTO customer_account_movements
             (company_id, customer_id, branch_id, movement_date, movement_type, account_currency_code,
              amount_usd, amount_ars, exchange_rate_value, sale_document_id, description, created_by)
             VALUES
             (:company_id, :customer_id, :branch_id, NOW(), "CREDIT", "USD",
              :amount_usd, :amount_ars, :exchange_rate_value, :sale_document_id, :description, :created_by)'
        );
        $stmt->execute([
            'company_id' => (int)$doc['company_id'],
            'customer_id' => (int)$doc['customer_id'],
            'branch_id' => (int)$doc['branch_id'],
            'amount_usd' => (float)$doc['total_usd_reference'],
            'amount_ars' => (float)$doc['total_ars'],
            'exchange_rate_value' => (float)($doc['account_exchange_rate_value'] ?? $doc['exchange_rate_value']),
            'sale_document_id' => (int)$doc['id'],
            'description' => 'Anulación ' . SaleType::label((string)$doc['document_type']) . ' ' . (string)$doc['document_number'],
            'created_by' => $userId,
        ]);
    }


    private function tableExists(PDO $pdo, string $table): bool
    {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?');
        $stmt->execute([$table]);
        return (int)$stmt->fetchColumn() > 0;
    }

    private function hasReceiptAllocations(PDO $pdo, int $saleDocumentId): bool
    {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM payment_allocations pa INNER JOIN receipts r ON r.id = pa.receipt_id WHERE pa.sale_document_id = :id AND r.status <> "VOID"');
        $stmt->execute(['id' => $saleDocumentId]);
        return (int)$stmt->fetchColumn() > 0;
    }

    private function sumReservedForDocument(PDO $pdo, int $saleDocumentId): float
    {
        $stmt = $pdo->prepare('SELECT COALESCE(SUM(quantity_reserved - quantity_released - quantity_delivered), 0) FROM stock_reservations WHERE sale_document_id = :id AND status = "ACTIVE"');
        $stmt->execute(['id' => $saleDocumentId]);
        return (float)$stmt->fetchColumn();
    }

    private function requireAnySalesViewPermission(): void
    {
        foreach ([SaleType::PRESUPUESTO, SaleType::PROFORMA_X, SaleType::VENTA_ANTICIPADA, SaleType::VENTA_MOSTRADOR] as $type) {
            if (Permission::has(SaleType::permissionBase($type) . '.ver')) {
                return;
            }
        }
        Permission::require('ventas.proformas.ver');
    }

    private function requireAnySalesCreatePermission(): void
    {
        foreach ([SaleType::PRESUPUESTO, SaleType::PROFORMA_X, SaleType::VENTA_ANTICIPADA, SaleType::VENTA_MOSTRADOR] as $type) {
            if (Permission::has(SaleType::permissionBase($type) . '.crear')) {
                return;
            }
        }
        Permission::require('ventas.proformas.crear');
    }

    private function requireViewPermission(string $type): void { Permission::require(SaleType::permissionBase($type) . '.ver'); }
    private function requireCreatePermission(string $type): void { Permission::require(SaleType::permissionBase($type) . '.crear'); }
    private function requireApprovePermission(string $type): void { Permission::require(SaleType::permissionBase($type) . '.aprobar'); }
    private function requireVoidPermission(string $type): void { Permission::require(SaleType::permissionBase($type) . '.anular'); }

    private function hasSaleDocumentItemColumn(PDO $pdo, string $column): bool
    {
        static $cache = [];
        if (array_key_exists($column, $cache)) {
            return $cache[$column];
        }
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = "sale_document_items" AND COLUMN_NAME = :column');
        $stmt->execute(['column' => $column]);
        $cache[$column] = ((int)$stmt->fetchColumn()) > 0;
        return $cache[$column];
    }

    private function hasSaleDocumentColumn(PDO $pdo, string $column): bool
    {
        static $cache = [];
        if (array_key_exists($column, $cache)) {
            return $cache[$column];
        }
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = "sale_documents" AND COLUMN_NAME = :column');
        $stmt->execute(['column' => $column]);
        $cache[$column] = ((int)$stmt->fetchColumn()) > 0;
        return $cache[$column];
    }

    private function dec(string $value): float
    {
        $text = trim($value);
        if ($text === '') {
            return 0.0;
        }
        if (str_contains($text, ',')) {
            $text = str_replace('.', '', $text);
            $text = str_replace(',', '.', $text);
        }
        return is_numeric($text) ? (float)$text : 0.0;
    }

    private function validDate(string $date): bool
    {
        $dt = \DateTimeImmutable::createFromFormat('Y-m-d', $date);
        return $dt instanceof \DateTimeImmutable && $dt->format('Y-m-d') === $date;
    }

    private function errorMessage(Throwable $e): string
    {
        return $e instanceof RuntimeException ? $e->getMessage() : 'Error interno: ' . $e->getMessage();
    }
}
