<?php

require_once __DIR__ . '/../../core/database.php';
require_once __DIR__ . '/../../core/config.php';
require_once __DIR__ . '/BillingHelpers.php';
require_once __DIR__ . '/BillingIssueValidationService.php';
require_once __DIR__ . '/BillingPaymentOptionsService.php';
require_once __DIR__ . '/../facto/FactoClient.php';
require_once __DIR__ . '/../facto/FactoFileStorageService.php';

class BillingIssueService
{
    private $pdo;
    private $validator;
    private $storage;

    public function __construct(PDO $pdo = null)
    {
        $this->pdo = $pdo ?: db();
        $this->validator = new BillingIssueValidationService($this->pdo);
        $this->storage = new FactoFileStorageService();
    }

    public function issueSandbox($quoteId, $documentKey, array $options = array())
    {
        $environment = billing_normalize_environment($options['environment'] ?? 'sandbox');
        if ($environment !== 'sandbox') {
            throw new RuntimeException('Paso 5 solo permite emision controlada en sandbox. Produccion sigue bloqueada.');
        }

        $confirmation = strtoupper(trim((string)($options['confirmation'] ?? '')));
        if ($confirmation !== 'EMITIR') {
            throw new RuntimeException('Confirmacion invalida. Debes escribir EMITIR para continuar.');
        }

        $validation = $this->validator->validateQuoteForIssue($quoteId, $documentKey, $options);
        if (empty($validation['ok'])) {
            return array(
                'ok' => false,
                'blocked' => true,
                'validation' => $validation,
                'message' => 'La validacion tributaria sigue bloqueada. Corrige los bloqueos antes de emitir en sandbox.'
            );
        }

        $previewLog = $this->findPreviewLog($validation['idempotency_key']);
        if (!$previewLog) {
            throw new RuntimeException('Debes generar un preview FACTO sandbox aprobado antes de confirmar la emision.');
        }

        $this->assertNoSuccessfulIssue($validation['idempotency_key']);

        $payload = $validation['payload_preview'];
        if (!isset($payload['options']) || !is_array($payload['options'])) {
            $payload['options'] = array();
        }
        unset($payload['options']['draft_preview'], $payload['options']['draft_only']);

        $markAsPaid = !empty($options['mark_as_paid']);
        $paymentDate = $this->cleanDate($options['payment_date'] ?? date('Y-m-d'));
        $paymentMethodId = (int)($options['payment_method_id'] ?? 0);
        $cashAccountId = (int)($options['cash_account_id'] ?? 0);
        $paymentReference = trim((string)($options['payment_reference'] ?? '')) ?: null;
        $paymentNotes = trim((string)($options['payment_notes'] ?? '')) ?: null;
        $paymentMethod = trim((string)($options['payment_method'] ?? '')) ?: null;
        $paymentMethodLabel = trim((string)($options['payment_method_label'] ?? '')) ?: null;
        $cashAccountLabel = trim((string)($options['cash_account_label'] ?? '')) ?: null;

        if ($markAsPaid) {
            if (!$paymentDate) {
                throw new RuntimeException('Debes informar la fecha de pago para registrar la factura como pagada.');
            }
            if ($paymentMethodId <= 0) {
                throw new RuntimeException('Debes seleccionar un metodo de pago estructurado.');
            }
            if ($cashAccountId <= 0) {
                throw new RuntimeException('Debes seleccionar la caja o cuenta destino del pago.');
            }
            $paymentOptions = new BillingPaymentOptionsService($this->pdo);
            $methodRow = $paymentOptions->getPaymentMethodById($paymentMethodId);
            if (!$methodRow) {
                throw new RuntimeException('El metodo de pago seleccionado no existe o esta inactivo.');
            }
            $cashAccountRow = $paymentOptions->getCashAccountById($cashAccountId);
            if (!$cashAccountRow) {
                throw new RuntimeException('La caja o cuenta destino seleccionada no existe o esta inactiva.');
            }
            $paymentMethod = $methodRow['method_key'];
            $paymentMethodLabel = $methodRow['method_name'];
            $cashAccountLabel = $cashAccountRow['account_name'];
        }

        $logId = $this->upsertIssueLog($validation, $payload, 'sent', null, null, null, null, null);

        try {
            $client = new FactoClient($environment);
            $response = $client->createDocument($payload, array('generate_stamp' => 1));
            $businessError = $this->extractBusinessError($response);
            if ($businessError) {
                $this->upsertIssueLog($validation, $payload, 'error', $response, null, null, null, null, 'FACTO_RESULT_ERROR', $businessError);
                return array(
                    'ok' => false,
                    'blocked' => false,
                    'validation' => $validation,
                    'message' => $businessError,
                    'facto_response_summary' => $this->responseSummary($response)
                );
            }

            $factoDocumentId = $this->extractDocumentId($response);
            if (!$factoDocumentId) {
                $this->upsertIssueLog($validation, $payload, 'pending_reconcile', $response, null, null, null, null, 'NO_DOCUMENT_ID', 'FACTO no devolvio document_id en la emision.');
                return array(
                    'ok' => false,
                    'pending_reconcile' => true,
                    'validation' => $validation,
                    'message' => 'FACTO no devolvio document_id. La emision queda pendiente de conciliacion.',
                    'facto_response_summary' => $this->responseSummary($response)
                );
            }

            $detailResponse = null;
            try {
                $detailResponse = $client->getDocument($factoDocumentId, true);
            } catch (Throwable $ignored) {
                $detailResponse = null;
            }

            $effectiveResponse = is_array($detailResponse) ? $detailResponse : $response;
            $documentNumber = $this->extractDocumentNumber($effectiveResponse) ?: $this->extractDocumentNumber($response);
            $electronicDocument = $this->extractElectronicDocument($effectiveResponse);
            if (empty($electronicDocument)) {
                $electronicDocument = $this->extractElectronicDocument($response);
            }

            $fileBase = $this->issueFileBaseName($validation, $factoDocumentId, $logId);
            $saved = $this->saveIssueFiles($electronicDocument, $environment, $fileBase);

            $status = 'success';
            $logId = $this->upsertIssueLog($validation, $payload, $status, $effectiveResponse, null, $factoDocumentId, $documentNumber, $saved);

            $this->pdo->beginTransaction();
            try {
                $this->persistFactoDocument($validation, $effectiveResponse, $factoDocumentId, $documentNumber, $saved, $markAsPaid, $paymentDate);
                $this->persistDocumentLines($validation, $factoDocumentId, $effectiveResponse);
                $collectionId = $this->persistCollection($validation, $factoDocumentId, $documentNumber, $markAsPaid, $paymentDate);
                if ($markAsPaid) {
                    $this->persistPayment($validation, $collectionId, $factoDocumentId, $paymentDate, $paymentMethod, $paymentMethodLabel, $paymentMethodId, $cashAccountId, $cashAccountLabel, $paymentReference, $paymentNotes);
                }
                $this->updateQuote($validation, $factoDocumentId, $documentNumber, $markAsPaid, $paymentDate, $paymentNotes);
                $this->updateOpportunity($validation, $factoDocumentId, $markAsPaid, $paymentDate);
                $this->insertCrmDocument($validation, $factoDocumentId, $documentNumber, $saved);
                $this->insertActivity($validation, $factoDocumentId, $documentNumber, $markAsPaid);
                $this->pdo->commit();
            } catch (Throwable $e) {
                if ($this->pdo->inTransaction()) {
                    $this->pdo->rollBack();
                }
                $this->upsertIssueLog($validation, $payload, 'pending_reconcile', $effectiveResponse, null, $factoDocumentId, $documentNumber, $saved, 'LOCAL_PERSIST_ERROR', $e->getMessage());
                return array(
                    'ok' => false,
                    'pending_reconcile' => true,
                    'validation' => $validation,
                    'issue' => array(
                        'log_id' => (int)$logId,
                        'environment' => $environment,
                        'facto_document_id' => $factoDocumentId,
                        'document_number' => $documentNumber,
                        'has_pdf' => !empty($saved['pdf_file_path']),
                        'has_xml' => !empty($saved['xml_file_path']),
                        'pdf_url' => !empty($saved['pdf_file_path']) ? '/modules/billing/preview_pdf.php?log_id=' . (int)$logId : null
                    ),
                    'message' => 'FACTO emitio el documento, pero ICYLAB OS no pudo guardar todo el cierre local. Queda pendiente de conciliacion. Error: ' . $e->getMessage(),
                    'facto_response_summary' => $this->responseSummary($effectiveResponse)
                );
            }

            $this->notifyIssue($validation, $factoDocumentId, $documentNumber, $markAsPaid);

            return array(
                'ok' => true,
                'blocked' => false,
                'validation' => $validation,
                'issue' => array(
                    'log_id' => (int)$logId,
                    'environment' => $environment,
                    'facto_document_id' => $factoDocumentId,
                    'document_number' => $documentNumber,
                    'has_pdf' => !empty($saved['pdf_file_path']),
                    'has_xml' => !empty($saved['xml_file_path']),
                    'pdf_url' => !empty($saved['pdf_file_path']) ? '/modules/billing/preview_pdf.php?log_id=' . (int)$logId : null,
                    'quote_status' => $markAsPaid ? 'Pagada' : 'Facturada',
                    'payment_status' => $markAsPaid ? 'pagado' : 'por_cobrar',
                    'collection_created' => true,
                    'payment_registered' => $markAsPaid,
                    'payment_method' => $markAsPaid ? $paymentMethodLabel : null,
                    'cash_account' => $markAsPaid ? $cashAccountLabel : null
                ),
                'message' => 'Documento emitido correctamente en FACTO sandbox y vinculado a ICYLAB OS.',
                'facto_response_summary' => $this->responseSummary($effectiveResponse)
            );
        } catch (FactoApiException $e) {
            $this->upsertIssueLog($validation, $payload, 'error', $e->getDecodedResponse() ?: $e->getResponseBody(), $e->getHttpCode(), null, null, null, 'FACTO_HTTP_ERROR', $e->getMessage());
            return array(
                'ok' => false,
                'blocked' => false,
                'validation' => $validation,
                'message' => $e->getMessage(),
                'http_code' => $e->getHttpCode(),
                'facto_error' => $e->getDecodedResponse()
            );
        }
    }

    private function findPreviewLog($idempotencyKey)
    {
        $stmt = $this->pdo->prepare("SELECT * FROM billing_issue_logs WHERE idempotency_key = ? AND mode = 'draft_preview' AND status = 'validated' AND pdf_file_path IS NOT NULL ORDER BY id DESC LIMIT 1");
        $stmt->execute(array($idempotencyKey));
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function assertNoSuccessfulIssue($idempotencyKey)
    {
        $stmt = $this->pdo->prepare("SELECT id, facto_document_id, document_number FROM billing_issue_logs WHERE idempotency_key = ? AND mode = 'issue' AND status IN ('sent','success','pending_reconcile') LIMIT 1");
        $stmt->execute(array($idempotencyKey));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($row) {
            throw new RuntimeException('Ya existe una emision para esta idempotency key. Log #' . $row['id'] . '.');
        }
    }

    private function upsertIssueLog(array $validation, array $payload, $status, $responsePayload = null, $httpCode = null, $factoDocumentId = null, $documentNumber = null, $savedFiles = null, $errorCode = null, $errorMessage = null)
    {
        $quote = $validation['quote'];
        $customer = $validation['customer'];
        $documentType = $validation['document_type'];
        $userId = function_exists('current_user_id') ? (int)current_user_id() : 0;
        $pdfPath = is_array($savedFiles) && !empty($savedFiles['pdf_file_path']) ? $savedFiles['pdf_file_path'] : null;
        $xmlPath = is_array($savedFiles) && !empty($savedFiles['xml_file_path']) ? $savedFiles['xml_file_path'] : null;
        $responseEncoded = $responsePayload === null ? null : $this->jsonEncode($responsePayload);

        $stmt = $this->pdo->prepare("\n            INSERT INTO billing_issue_logs\n            (quote_id, opportunity_id, customer_id, document_type_key, document_type_id, environment, mode, status, idempotency_key, request_payload, response_payload, http_code, facto_document_id, document_number, pdf_file_path, xml_file_path, error_code, error_message, created_by)\n            VALUES\n            (?, ?, ?, ?, ?, ?, 'issue', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n            ON DUPLICATE KEY UPDATE\n                mode = VALUES(mode),\n                status = VALUES(status),\n                request_payload = VALUES(request_payload),\n                response_payload = VALUES(response_payload),\n                http_code = VALUES(http_code),\n                facto_document_id = COALESCE(VALUES(facto_document_id), facto_document_id),\n                document_number = COALESCE(VALUES(document_number), document_number),\n                pdf_file_path = COALESCE(VALUES(pdf_file_path), pdf_file_path),\n                xml_file_path = COALESCE(VALUES(xml_file_path), xml_file_path),\n                error_code = VALUES(error_code),\n                error_message = VALUES(error_message),\n                updated_at = CURRENT_TIMESTAMP\n        ");

        $stmt->execute(array(
            (int)$quote['id'],
            isset($quote['opportunity_id']) ? (int)$quote['opportunity_id'] : null,
            isset($customer['id']) ? (int)$customer['id'] : null,
            $documentType['document_key'],
            (int)$documentType['facto_document_type_id'],
            $validation['environment'],
            $status,
            $validation['idempotency_key'],
            $this->jsonEncode($payload),
            $responseEncoded,
            $httpCode,
            $factoDocumentId,
            $documentNumber,
            $pdfPath,
            $xmlPath,
            $errorCode,
            $errorMessage,
            $userId ?: null
        ));

        $stmt = $this->pdo->prepare('SELECT id FROM billing_issue_logs WHERE idempotency_key = ? LIMIT 1');
        $stmt->execute(array($validation['idempotency_key']));
        return (int)$stmt->fetchColumn();
    }

    private function persistFactoDocument(array $validation, array $response, $factoDocumentId, $documentNumber, array $saved, $markAsPaid, $paymentDate)
    {
        $quote = $validation['quote'];
        $customer = $validation['customer'];
        $docType = $validation['document_type'];
        $calc = $validation['calculation'];
        $payment = $validation['payment'];
        $header = $this->extractHeader($response);
        $totals = $this->extractTotals($response);
        $raw = $this->jsonEncode($response);

        $stmt = $this->pdo->prepare("\n            INSERT INTO facto_documents\n            (document_id, environment, account_id, issue_date, document_number, document_type_id, issuer_tax_id_code, receiver_legal_name, receiver_tax_id_code, customer_supplier_id, crm_customer_id, crm_quote_id, crm_opportunity_id, finance_center_id, link_status, link_confidence, linked_by_user_id, linked_at, document_status, currency_id, received_issued_flag, net_amount, taxes_amount, total_amount, payment_status, payment_due_date, payment_paid_date, document_type_taxbureau, taxbureau_sending_status, taxbureau_validation_status, document_sequence_code, pdf_file_path, xml_file_path, raw_payload, payload_hash, last_synced_at)\n            VALUES\n            (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'auto_linked', 100.00, ?, NOW(), ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, SHA2(?, 256), NOW())\n            ON DUPLICATE KEY UPDATE\n                environment = VALUES(environment),\n                account_id = VALUES(account_id),\n                issue_date = VALUES(issue_date),\n                document_number = VALUES(document_number),\n                document_type_id = VALUES(document_type_id),\n                issuer_tax_id_code = VALUES(issuer_tax_id_code),\n                receiver_legal_name = VALUES(receiver_legal_name),\n                receiver_tax_id_code = VALUES(receiver_tax_id_code),\n                customer_supplier_id = VALUES(customer_supplier_id),\n                crm_customer_id = VALUES(crm_customer_id),\n                crm_quote_id = VALUES(crm_quote_id),\n                crm_opportunity_id = VALUES(crm_opportunity_id),\n                finance_center_id = VALUES(finance_center_id),\n                link_status = VALUES(link_status),\n                link_confidence = VALUES(link_confidence),\n                linked_by_user_id = VALUES(linked_by_user_id),\n                linked_at = VALUES(linked_at),\n                document_status = VALUES(document_status),\n                currency_id = VALUES(currency_id),\n                received_issued_flag = VALUES(received_issued_flag),\n                net_amount = VALUES(net_amount),\n                taxes_amount = VALUES(taxes_amount),\n                total_amount = VALUES(total_amount),\n                payment_status = VALUES(payment_status),\n                payment_due_date = VALUES(payment_due_date),\n                payment_paid_date = VALUES(payment_paid_date),\n                document_type_taxbureau = VALUES(document_type_taxbureau),\n                taxbureau_sending_status = VALUES(taxbureau_sending_status),\n                taxbureau_validation_status = VALUES(taxbureau_validation_status),\n                document_sequence_code = VALUES(document_sequence_code),\n                pdf_file_path = VALUES(pdf_file_path),\n                xml_file_path = VALUES(xml_file_path),\n                raw_payload = VALUES(raw_payload),\n                payload_hash = VALUES(payload_hash),\n                last_synced_at = NOW()\n        ");

        $userId = function_exists('current_user_id') ? (int)current_user_id() : null;
        $stmt->execute(array(
            $factoDocumentId,
            $validation['environment'],
            $header['account_id'] ?? null,
            $header['issue_date'] ?? $payment['issue_date'],
            $documentNumber,
            $header['document_type_id'] ?? $docType['facto_document_type_id'],
            $header['issuer_tax_id_code'] ?? null,
            $header['receiver_legal_name'] ?? $customer['name'],
            $header['receiver_tax_id_code'] ?? $customer['tax_id'],
            $header['customer_supplier_id'] ?? null,
            $customer['id'],
            $quote['id'],
            $quote['opportunity_id'] ?? null,
            $this->resolveFinanceCenterId($validation),
            $userId ?: null,
            $header['document_status'] ?? 1,
            $header['currency_id'] ?? null,
            $totals['net_amount'] ?? $calc['net_amount'],
            $totals['taxes_amount'] ?? $calc['taxes_amount'],
            $totals['total_amount'] ?? $calc['total_amount'],
            $markAsPaid ? 'pagado' : 'por_cobrar',
            $payment['due_date'] ?? null,
            $markAsPaid ? $paymentDate : null,
            $header['document_type_taxbureau'] ?? $docType['sii_external_code'],
            $header['taxbureau_sending_status'] ?? null,
            $header['taxbureau_validation_status'] ?? null,
            $header['document_sequence_code'] ?? null,
            $saved['pdf_file_path'] ?? null,
            $saved['xml_file_path'] ?? null,
            $raw,
            $raw
        ));
    }

    private function persistDocumentLines(array $validation, $factoDocumentId, array $response)
    {
        $this->pdo->prepare('DELETE FROM billing_document_lines WHERE environment = ? AND facto_document_id = ?')
            ->execute(array($validation['environment'], $factoDocumentId));

        $stmt = $this->pdo->prepare("\n            INSERT INTO billing_document_lines\n            (environment, facto_document_id, quote_id, source_line_id, product_id, sku, description, quantity, unit_measure, unit_price, discount_percent, discount_amount, net_amount, tax_amount, total_amount, tax_treatment, finance_center_id, raw_payload)\n            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n        ");

        foreach (($validation['calculation']['lines'] ?? array()) as $line) {
            $stmt->execute(array(
                $validation['environment'],
                $factoDocumentId,
                $validation['quote']['id'],
                $line['source_line_id'] ?? null,
                $line['product_id'] ?? null,
                $line['sku'] ?? null,
                $line['description'] ?? '',
                $line['quantity'] ?? 0,
                $line['unit_measure'] ?? 'UN',
                $line['unit_price'] ?? 0,
                $line['discount_percent'] ?? 0,
                ($line['line_discount_amount'] ?? 0) + ($line['global_discount_allocated'] ?? 0),
                $line['net_amount'] ?? 0,
                $line['tax_amount'] ?? 0,
                $line['gross_amount'] ?? 0,
                $line['tax_treatment'] ?? 'afecto',
                $line['finance_center_id'] ?? $this->resolveFinanceCenterId($validation),
                $this->jsonEncode($line)
            ));
        }
    }

    private function persistCollection(array $validation, $factoDocumentId, $documentNumber, $markAsPaid, $paymentDate)
    {
        $calc = $validation['calculation'];
        $payment = $validation['payment'];
        $quote = $validation['quote'];
        $customer = $validation['customer'];
        $status = $markAsPaid ? 'pagado' : 'por_cobrar';
        $paidAmount = $markAsPaid ? $calc['total_amount'] : 0;
        $balance = $markAsPaid ? 0 : $calc['total_amount'];

        $stmt = $this->pdo->prepare("\n            INSERT INTO billing_collections\n            (environment, direction, source_table, facto_document_id, quote_id, opportunity_id, customer_id, document_number, document_type_id, issue_date, due_date, total_amount, paid_amount, balance_amount, collection_status, payment_terms, last_payment_date, finance_center_id, notes)\n            VALUES (?, 'receivable', 'facto_documents', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n            ON DUPLICATE KEY UPDATE\n                quote_id = VALUES(quote_id),\n                opportunity_id = VALUES(opportunity_id),\n                customer_id = VALUES(customer_id),\n                document_number = VALUES(document_number),\n                document_type_id = VALUES(document_type_id),\n                issue_date = VALUES(issue_date),\n                due_date = VALUES(due_date),\n                total_amount = VALUES(total_amount),\n                paid_amount = VALUES(paid_amount),\n                balance_amount = VALUES(balance_amount),\n                collection_status = VALUES(collection_status),\n                payment_terms = VALUES(payment_terms),\n                last_payment_date = VALUES(last_payment_date),\n                finance_center_id = VALUES(finance_center_id),\n                notes = VALUES(notes),\n                updated_at = CURRENT_TIMESTAMP\n        ");
        $stmt->execute(array(
            $validation['environment'],
            $factoDocumentId,
            $quote['id'],
            $quote['opportunity_id'] ?? null,
            $customer['id'],
            $documentNumber,
            $validation['document_type']['facto_document_type_id'],
            $payment['issue_date'],
            $payment['due_date'],
            $calc['total_amount'],
            $paidAmount,
            $balance,
            $status,
            $payment['payment_conditions'],
            $markAsPaid ? $paymentDate : null,
            $this->resolveFinanceCenterId($validation),
            'Creado automaticamente por emision sandbox desde ICYLAB OS.'
        ));

        $stmt = $this->pdo->prepare('SELECT id FROM billing_collections WHERE environment = ? AND direction = ? AND facto_document_id = ? LIMIT 1');
        $stmt->execute(array($validation['environment'], 'receivable', $factoDocumentId));
        return (int)$stmt->fetchColumn();
    }

    private function persistPayment(array $validation, $collectionId, $factoDocumentId, $paymentDate, $paymentMethod, $paymentMethodLabel, $paymentMethodId, $cashAccountId, $cashAccountLabel, $paymentReference, $paymentNotes)
    {
        $cols = array('collection_id', 'environment', 'direction', 'facto_document_id', 'quote_id', 'opportunity_id', 'customer_id', 'payment_date', 'amount', 'payment_method', 'external_reference', 'finance_center_id', 'notes', 'created_by');
        $vals = array(
            $collectionId ?: null,
            $validation['environment'],
            'received',
            $factoDocumentId,
            $validation['quote']['id'],
            $validation['quote']['opportunity_id'] ?? null,
            $validation['customer']['id'],
            $paymentDate,
            $validation['calculation']['total_amount'],
            $paymentMethodLabel ?: $paymentMethod,
            $paymentReference,
            $this->resolveFinanceCenterId($validation),
            $paymentNotes,
            function_exists('current_user_id') ? (int)current_user_id() : null
        );

        if (billing_table_has_column($this->pdo, 'billing_payments', 'payment_method_id')) {
            $cols[] = 'payment_method_id';
            $vals[] = $paymentMethodId ?: null;
        }
        if (billing_table_has_column($this->pdo, 'billing_payments', 'cash_account_id')) {
            $cols[] = 'cash_account_id';
            $vals[] = $cashAccountId ?: null;
        }

        $placeholders = implode(',', array_fill(0, count($cols), '?'));
        $sql = 'INSERT INTO billing_payments (' . implode(',', $cols) . ') VALUES (' . $placeholders . ')';
        $this->pdo->prepare($sql)->execute($vals);
    }

    private function updateQuote(array $validation, $factoDocumentId, $documentNumber, $markAsPaid, $paymentDate, $paymentNotes)
    {
        $quote = $validation['quote'];
        $payment = $validation['payment'];
        $calc = $validation['calculation'];
        $status = $markAsPaid ? 'Pagada' : 'Facturada';
        $paymentStatus = $markAsPaid ? 'pagado' : 'por_cobrar';
        $userId = function_exists('current_user_id') ? (int)current_user_id() : null;

        $stmt = $this->pdo->prepare("\n            UPDATE crm_quotes SET\n                status = ?,\n                invoice_number = ?,\n                invoice_issue_date = ?,\n                facto_document_id = ?,\n                billing_document_type = ?,\n                billing_document_type_id = ?,\n                billing_environment = ?,\n                payment_status = ?,\n                payment_terms_selected = ?,\n                payment_due_date = ?,\n                payment_paid_date = ?,\n                payment_amount = ?,\n                payment_notes = ?,\n                billing_match_status = 'matched',\n                facto_linked_at = NOW(),\n                facto_linked_by = ?,\n                emitted_by_user_id = ?,\n                emitted_at = NOW(),\n                finance_center_id = ?\n            WHERE id = ?\n        ");
        $stmt->execute(array(
            $status,
            $documentNumber,
            $payment['issue_date'],
            $factoDocumentId,
            $validation['document_type']['document_key'],
            $validation['document_type']['facto_document_type_id'],
            $validation['environment'],
            $paymentStatus,
            $payment['payment_conditions'],
            $payment['due_date'],
            $markAsPaid ? $paymentDate : null,
            $markAsPaid ? $calc['total_amount'] : null,
            $paymentNotes,
            $userId ?: null,
            $userId ?: null,
            $this->resolveFinanceCenterId($validation),
            $quote['id']
        ));
    }

    private function updateOpportunity(array $validation, $factoDocumentId, $markAsPaid, $paymentDate)
    {
        $opportunityId = isset($validation['quote']['opportunity_id']) ? (int)$validation['quote']['opportunity_id'] : 0;
        if ($opportunityId <= 0) {
            return;
        }

        $stmt = $this->pdo->prepare("\n            UPDATE crm_opportunities SET\n                stage = 'facturada',\n                payment_status = ?,\n                payment_due_date = ?,\n                payment_paid_date = ?,\n                facto_document_id = ?,\n                finance_center_id = ?\n            WHERE id = ?\n        ");
        $stmt->execute(array(
            $markAsPaid ? 'pagado' : 'por_cobrar',
            $validation['payment']['due_date'] ?? null,
            $markAsPaid ? $paymentDate : null,
            $factoDocumentId,
            $this->resolveFinanceCenterId($validation),
            $opportunityId
        ));
    }

    private function insertCrmDocument(array $validation, $factoDocumentId, $documentNumber, array $saved)
    {
        $cols = array('opportunity_id', 'customer_id', 'document_type', 'document_name', 'document_number', 'document_date', 'net_amount', 'total_amount', 'file_path', 'facto_document_id', 'notes');
        $vals = array(
            $validation['quote']['opportunity_id'] ?? null,
            $validation['customer']['id'],
            'factura',
            'Factura FACTO ' . ($documentNumber ?: $factoDocumentId),
            $documentNumber,
            $validation['payment']['issue_date'],
            $validation['calculation']['net_amount'],
            $validation['calculation']['total_amount'],
            $saved['pdf_file_path'] ?? null,
            $factoDocumentId,
            'Documento emitido desde ICYLAB OS en sandbox. Cotizacion ' . $validation['quote']['quote_number'] . '.'
        );

        if (billing_table_has_column($this->pdo, 'crm_documents', 'quote_id')) {
            $cols[] = 'quote_id';
            $vals[] = $validation['quote']['id'];
        }
        if (billing_table_has_column($this->pdo, 'crm_documents', 'link_status')) {
            $cols[] = 'link_status';
            $vals[] = 'auto';
        }
        if (billing_table_has_column($this->pdo, 'crm_documents', 'finance_center_id')) {
            $cols[] = 'finance_center_id';
            $vals[] = $this->resolveFinanceCenterId($validation);
        }

        $placeholders = implode(',', array_fill(0, count($cols), '?'));
        $sql = 'INSERT INTO crm_documents (' . implode(',', $cols) . ') VALUES (' . $placeholders . ')';
        $this->pdo->prepare($sql)->execute($vals);
    }

    private function insertActivity(array $validation, $factoDocumentId, $documentNumber, $markAsPaid)
    {
        if (!billing_table_has_column($this->pdo, 'crm_activities', 'title')) {
            return;
        }
        $quote = $validation['quote'];
        $customer = $validation['customer'];
        $title = 'Documento tributario emitido (' . $quote['quote_number'] . ')';
        $description = "Factura sandbox emitida desde ICYLAB OS.\n" .
            'FACTO document_id: ' . $factoDocumentId . "\n" .
            'Folio: ' . ($documentNumber ?: 'no informado') . "\n" .
            'Total: $' . number_format((float)$validation['calculation']['total_amount'], 0, ',', '.') . "\n" .
            'Cobranza: ' . ($markAsPaid ? 'pagada' : 'por cobrar') . '.';

        $stmt = $this->pdo->prepare("\n            INSERT INTO crm_activities\n            (opportunity_id, customer_id, customer_name, activity_type, title, description, activity_date, owner_name)\n            VALUES (?, ?, ?, 'factura', ?, ?, NOW(), ?)\n        ");
        $stmt->execute(array(
            $quote['opportunity_id'] ?? null,
            $customer['id'],
            $customer['name'],
            $title,
            $description,
            $this->currentOwnerName()
        ));
    }

    private function notifyIssue(array $validation, $factoDocumentId, $documentNumber, $markAsPaid)
    {
        $servicePath = __DIR__ . '/../notifications/NotificationService.php';
        if (!is_file($servicePath)) {
            return;
        }
        require_once $servicePath;
        if (!class_exists('NotificationService')) {
            return;
        }
        try {
            $notifier = new NotificationService($this->pdo);
            $notifier->notify($markAsPaid ? 'billing.paid' : 'billing.por_cobrar', array(
                'module' => 'billing',
                'priority' => $markAsPaid ? 'success' : 'info',
                'title' => 'Factura emitida desde ICYLAB OS',
                'body' => 'Cotizacion ' . $validation['quote']['quote_number'] . ' emitida en sandbox. Folio ' . ($documentNumber ?: '-') . ', FACTO document_id ' . $factoDocumentId . '.',
                'url' => '/modules/billing/',
                'related_type' => 'quote',
                'related_id' => $validation['quote']['id'],
                'actor_user_id' => function_exists('current_user_id') ? (int)current_user_id() : null,
                'dedupe_key' => 'billing.issue:' . $validation['environment'] . ':' . $factoDocumentId
            ));
        } catch (Throwable $ignored) {
        }
    }

    private function resolveFinanceCenterId(array $validation)
    {
        if (!empty($validation['quote']['finance_center_id'])) return (int)$validation['quote']['finance_center_id'];
        if (!empty($validation['customer']['default_income_center_id'])) return (int)$validation['customer']['default_income_center_id'];
        $stmt = $this->pdo->query("SELECT id FROM finance_centers WHERE center_code = 'NO_INCOME_CENTER' LIMIT 1");
        $id = $stmt ? (int)$stmt->fetchColumn() : 0;
        if ($id > 0) return $id;
        $stmt = $this->pdo->query("SELECT id FROM finance_centers WHERE is_default = 1 ORDER BY id ASC LIMIT 1");
        return $stmt ? ((int)$stmt->fetchColumn() ?: null) : null;
    }

    private function saveIssueFiles(array $electronicDocument, $environment, $fileBase)
    {
        $saved = array('pdf_file_path' => null, 'xml_file_path' => null);
        if (!empty($electronicDocument['document_pdf'])) {
            $saved['pdf_file_path'] = $this->storage->saveBase64File($electronicDocument['document_pdf'], $environment, 'issued_pdf', $fileBase . '.pdf');
        }
        if (!empty($electronicDocument['document_xml'])) {
            $saved['xml_file_path'] = $this->storage->saveBase64File($electronicDocument['document_xml'], $environment, 'issued_xml', $fileBase . '.xml');
        }
        return $saved;
    }

    private function extractElectronicDocument($response)
    {
        if (!is_array($response)) return array();
        if (isset($response['electronic_document']) && is_array($response['electronic_document'])) return $response['electronic_document'];
        if (isset($response['document']['electronic_document']) && is_array($response['document']['electronic_document'])) return $response['document']['electronic_document'];
        $doc = array();
        foreach (array('document_pdf','document_xml','stamp_img','document_transfer_copy_pdf') as $key) {
            if (!empty($response[$key])) $doc[$key] = $response[$key];
        }
        return $doc;
    }

    private function extractDocumentId($response)
    {
        if (!is_array($response)) return null;
        if (!empty($response['document_id'])) return (string)$response['document_id'];
        if (!empty($response['header']['document_id'])) return (string)$response['header']['document_id'];
        if (!empty($response['document']['document_id'])) return (string)$response['document']['document_id'];
        return null;
    }

    private function extractDocumentNumber($response)
    {
        if (!is_array($response)) return null;
        if (!empty($response['document_number'])) return (string)$response['document_number'];
        if (!empty($response['header']['document_number'])) return (string)$response['header']['document_number'];
        if (!empty($response['document']['header']['document_number'])) return (string)$response['document']['header']['document_number'];
        return null;
    }

    private function extractHeader($response)
    {
        if (!is_array($response)) return array();
        if (isset($response['header']) && is_array($response['header'])) return $response['header'];
        if (isset($response['document']['header']) && is_array($response['document']['header'])) return $response['document']['header'];
        return array();
    }

    private function extractTotals($response)
    {
        if (!is_array($response)) return array();
        if (isset($response['totals']) && is_array($response['totals'])) return $response['totals'];
        if (isset($response['document']['totals']) && is_array($response['document']['totals'])) return $response['document']['totals'];
        return array();
    }

    private function extractBusinessError($response)
    {
        if (!is_array($response)) return null;
        if (!empty($response['result']['error_message'])) return (string)$response['result']['error_message'];
        if (!empty($response['error_message'])) return (string)$response['error_message'];
        return null;
    }

    private function responseSummary($response)
    {
        return array(
            'document_id' => $this->extractDocumentId($response),
            'document_number' => $this->extractDocumentNumber($response),
            'has_electronic_document' => !empty($this->extractElectronicDocument($response)),
            'result_status' => is_array($response) && isset($response['result']['status']) ? $response['result']['status'] : null,
            'result_error_message' => is_array($response) && isset($response['result']['error_message']) ? $response['result']['error_message'] : null,
            'top_level_keys' => is_array($response) ? array_keys($response) : array()
        );
    }

    private function issueFileBaseName(array $validation, $factoDocumentId, $logId)
    {
        $quoteNumber = preg_replace('/[^A-Za-z0-9_\-]/', '_', (string)$validation['quote']['quote_number']);
        return 'issue_' . $quoteNumber . '_facto_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', (string)$factoDocumentId) . '_log_' . (int)$logId;
    }

    private function cleanDate($value)
    {
        $ts = strtotime((string)$value);
        return $ts ? date('Y-m-d', $ts) : date('Y-m-d');
    }

    private function currentOwnerName()
    {
        if (function_exists('current_user_name')) {
            $name = trim((string)current_user_name());
            if ($name !== '') return $name;
        }
        if (!empty($_SESSION['user']['name'])) return (string)$_SESSION['user']['name'];
        if (!empty($_SESSION['user_name'])) return (string)$_SESSION['user_name'];
        return 'Sistema';
    }

    private function jsonEncode($value)
    {
        return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
}
