<?php

require_once __DIR__ . '/../../core/database.php';
require_once __DIR__ . '/../../core/config.php';
require_once __DIR__ . '/BillingHelpers.php';
require_once __DIR__ . '/BillingRuntimeGate.php';
require_once __DIR__ . '/BillingDocumentTypeService.php';
require_once __DIR__ . '/BillingTaxDocumentFactory.php';
require_once __DIR__ . '/FactoDocumentResponseParser.php';
require_once __DIR__ . '/../facto/FactoClient.php';
require_once __DIR__ . '/../facto/FactoFileStorageService.php';

class BillingDispatchGuideService
{
    private $pdo;
    private $types;
    private $factory;
    private $storage;

    public function __construct(?PDO $pdo = null)
    {
        $this->pdo = $pdo ?: db();
        $this->types = new BillingDocumentTypeService($this->pdo);
        $this->factory = new BillingTaxDocumentFactory();
        $this->storage = new FactoFileStorageService();
    }

    public function searchSources($sourceType, $search = '', $environment = 'sandbox')
    {
        $environment = $this->normalizeEnvironment($environment);
        $sourceType = $this->normalizeSourceType($sourceType);
        $search = trim((string)$search);
        $needle = '%' . $search . '%';

        if ($sourceType === 'invoice') {
            $sql = "
                SELECT fd.id,fd.document_number,fd.issue_date,fd.total_amount,fd.receiver_legal_name,
                       fd.receiver_tax_id_code,fd.crm_quote_id,q.quote_number,c.customer_name,c.legal_name
                FROM facto_documents fd
                LEFT JOIN crm_quotes q ON q.id=fd.crm_quote_id
                LEFT JOIN crm_customers c ON c.id=fd.crm_customer_id
                WHERE fd.environment=?
                  AND CAST(fd.document_type_id AS UNSIGNED) IN (2,32)
                  AND (fd.document_status IS NULL OR fd.document_status<>0)
            ";
            $params = array($environment);
            if ($search !== '') {
                $sql .= " AND (fd.document_number LIKE ? OR fd.receiver_legal_name LIKE ? OR fd.receiver_tax_id_code LIKE ? OR q.quote_number LIKE ?)";
                array_push($params, $needle, $needle, $needle, $needle);
            }
            $sql .= " ORDER BY fd.issue_date DESC,fd.id DESC LIMIT 50";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            $rows = array();
            foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
                $rows[] = array(
                    'source_type' => 'invoice',
                    'id' => (int)$row['id'],
                    'number' => $row['document_number'],
                    'date' => $row['issue_date'],
                    'customer' => $row['legal_name'] ?: ($row['customer_name'] ?: $row['receiver_legal_name']),
                    'tax_id' => $row['receiver_tax_id_code'],
                    'quote_number' => $row['quote_number'],
                    'total_amount' => billing_money($row['total_amount'])
                );
            }
            return $rows;
        }

        $sql = "
            SELECT q.id,q.quote_number,q.created_at,q.total,q.status,c.customer_name,c.legal_name,c.customer_tax_id
            FROM crm_quotes q
            INNER JOIN crm_customers c ON c.id=q.customer_id
            WHERE q.deleted_at IS NULL
              AND LOWER(q.status) IN ('aceptada','pagada','facturada')
        ";
        $params = array();
        if ($search !== '') {
            $sql .= " AND (q.quote_number LIKE ? OR c.customer_name LIKE ? OR c.legal_name LIKE ? OR c.customer_tax_id LIKE ?)";
            array_push($params, $needle, $needle, $needle, $needle);
        }
        $sql .= " ORDER BY q.created_at DESC,q.id DESC LIMIT 50";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        $rows = array();
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $rows[] = array(
                'source_type' => 'quote',
                'id' => (int)$row['id'],
                'number' => $row['quote_number'],
                'date' => substr((string)$row['created_at'], 0, 10),
                'customer' => $row['legal_name'] ?: $row['customer_name'],
                'tax_id' => $row['customer_tax_id'],
                'status' => $row['status'],
                'total_amount' => billing_money($row['total'])
            );
        }
        return $rows;
    }

    public function getSource($sourceType, $sourceId, $environment = 'sandbox')
    {
        $sourceType = $this->normalizeSourceType($sourceType);
        $environment = $this->normalizeEnvironment($environment);
        $sourceId = (int)$sourceId;
        if ($sourceId <= 0) {
            throw new InvalidArgumentException('Origen de guía inválido.');
        }

        return $sourceType === 'invoice'
            ? $this->loadInvoiceSource($sourceId, $environment)
            : $this->loadQuoteSource($sourceId, $environment);
    }

    public function previewGuide(array $data, $userId = null)
    {
        $environment = $this->normalizeEnvironment($data['environment'] ?? 'sandbox');
        billing_assert_runtime_operation('preview', $environment, 'Preview de guía de despacho');
        $context = $this->prepareContext($data, $environment);
        $payload = $context['payload'];
        $previewPayload = $payload;
        if (!isset($previewPayload['options']) || !is_array($previewPayload['options'])) {
            $previewPayload['options'] = array();
        }
        $previewPayload['options']['draft_preview'] = true;

        $guide = $this->createOrResetGuide($context, $previewPayload, $payload, $userId);
        $response = null;
        try {
            $client = new FactoClient($environment);
            $response = $client->createDocumentDraftPreview($previewPayload, array('generate_stamp' => 1));
            $parser = new FactoDocumentResponseParser(is_array($response) ? $response : array());
            $parser->assertPreviewReady();

            $previewDocumentId = $parser->documentId();
            $previewDocumentNumber = $parser->documentNumber();
            $saved = $this->storage->saveElectronicDocumentFiles(
                'phase8_2_guide_preview_' . (int)$guide['id'] . '_' . $previewDocumentId,
                $parser->electronicDocument(),
                $environment
            );

            $stmt = $this->pdo->prepare("
                UPDATE billing_dispatch_guides
                SET status='preview_ready',preview_response_payload=?,preview_facto_document_id=?,
                    preview_document_number=?,preview_pdf_file_path=?,preview_xml_file_path=?,preview_transfer_copy_pdf_file_path=?,
                    error_code=NULL,error_message=NULL,updated_at=NOW()
                WHERE id=?
            ");
            $stmt->execute(array(
                $this->jsonEncode($parser->safeResponse()),
                $previewDocumentId,
                $previewDocumentNumber,
                $saved['pdf_file_path'] ?? null,
                $saved['xml_file_path'] ?? null,
                $saved['transfer_copy_pdf_file_path'] ?? null,
                (int)$guide['id']
            ));

            return array(
                'guide_id' => (int)$guide['id'],
                'status' => 'preview_ready',
                'environment' => $environment,
                'source' => $context['source_summary'],
                'guide_type' => $context['guide_type'],
                'guide_type_label' => $this->guideTypeLabel($context['guide_type']),
                'transfer_type' => $context['transfer_type'],
                'transfer_type_label' => $this->transferTypeLabel($context['transfer_type']),
                'destination' => $context['destination'],
                'amounts' => $context['totals'],
                'line_count' => count($context['lines']),
                'preview' => array(
                    'document_id' => $previewDocumentId,
                    'document_number' => $previewDocumentNumber,
                    'pdf_available' => !empty($saved['pdf_file_path']),
                    'xml_available' => !empty($saved['xml_file_path']),
                    'transfer_copy_pdf_available' => !empty($saved['transfer_copy_pdf_file_path'])
                )
            );
        } catch (Throwable $e) {
            $this->markError((int)$guide['id'], 'preview_error', $e->getMessage(), $response, 'preview');
            throw $e;
        }
    }

    public function issueGuide($guideId, $confirmation, $userId = null)
    {
        $guideId = (int)$guideId;
        if ($guideId <= 0) {
            throw new InvalidArgumentException('Guía inválida.');
        }

        $this->pdo->beginTransaction();
        try {
            $guide = $this->getGuideForUpdate($guideId);
            billing_assert_runtime_operation('issue', $guide['environment'] ?? 'sandbox', 'Emisión de guía de despacho');
            billing_assert_issue_confirmation($confirmation, $guide['environment'] ?? 'sandbox');
            if ($guide['status'] === 'issued') {
                $this->pdo->commit();
                return $this->guideResult($guide);
            }
            if ($guide['status'] === 'remote_uncertain') {
                throw new RuntimeException('La emisión quedó con resultado remoto incierto. No se puede reenviar para evitar una guía duplicada.');
            }
            if ($guide['status'] !== 'preview_ready') {
                throw new RuntimeException('La guía debe tener un preview válido antes de emitir.');
            }
            $payload = json_decode((string)$guide['issue_request_payload'], true);
            if (!is_array($payload)) {
                throw new RuntimeException('No se encontró el payload validado de la guía.');
            }
            $this->assertSourceStillValid($guide);
            $this->pdo->prepare("UPDATE billing_dispatch_guides SET status='issuing',issued_by=?,updated_at=NOW() WHERE id=?")
                ->execute(array($userId ?: null, $guideId));
            $this->pdo->commit();
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $e;
        }

        $response = null;
        $remoteDocumentId = null;
        try {
            $client = new FactoClient($guide['environment']);
            $response = $client->createDocument($payload, array('generate_stamp' => 1));
            $parser = new FactoDocumentResponseParser(is_array($response) ? $response : array());
            $parser->assertIssuedReady();
            $remoteDocumentId = $parser->documentId();

            $detailResponse = $client->getDocument($remoteDocumentId, true);
            $detailParser = new FactoDocumentResponseParser(is_array($detailResponse) ? $detailResponse : array());
            $detailParser->assertSuccess('consulta de la guía emitida');
            $this->validateIssuedGuide($guide, $detailParser);

            $effective = $detailParser->documentId() !== null ? $detailParser : $parser;
            $electronic = $effective->electronicDocument();
            if (!$effective->hasPdf() && !$parser->hasPdf()) {
                throw new RuntimeException('FACTO emitió la guía, pero no devolvió PDF verificable. Quedó pendiente el cierre local.');
            }
            if (empty($electronic)) {
                $electronic = $parser->electronicDocument();
            }
            $saved = $this->storage->saveElectronicDocumentFiles(
                'phase8_2_guide_issued_' . $guideId . '_' . $remoteDocumentId,
                $electronic,
                $guide['environment']
            );
            $documentNumber = $effective->documentNumber() ?: $parser->documentNumber();
            $this->finalizeLocalIssue(
                $guideId,
                $payload,
                is_array($detailResponse) ? $detailResponse : $response,
                $remoteDocumentId,
                $documentNumber,
                $saved,
                $userId
            );
            return $this->guideResult($this->getGuide($guideId));
        } catch (Throwable $e) {
            if ($remoteDocumentId !== null) {
                $this->markPendingLocalCommit($guideId, $remoteDocumentId, $response, $e->getMessage());
            } else {
                $parser = is_array($response) ? new FactoDocumentResponseParser($response) : null;
                if ($parser && $parser->resultStatus() === 0) {
                    $this->markRemoteUncertain($guideId, $response, $e->getMessage());
                } else {
                    $this->markError($guideId, 'issue_error', $e->getMessage(), $response, 'issue');
                }
            }
            throw $e;
        }
    }

    public function repairLocalCommit($guideId, $userId = null)
    {
        $guide = $this->getGuide((int)$guideId);
        billing_assert_runtime_operation('finance', $guide['environment'] ?? 'sandbox', 'Reparación del cierre local de guía de despacho');
        if ($guide['status'] !== 'pending_local_commit') {
            throw new RuntimeException('La guía no requiere reparación local.');
        }
        if (empty($guide['facto_document_id'])) {
            throw new RuntimeException('No existe document_id remoto para reparar.');
        }
        $payload = json_decode((string)$guide['issue_request_payload'], true);
        if (!is_array($payload)) {
            throw new RuntimeException('No se encontró el payload original.');
        }
        $client = new FactoClient($guide['environment']);
        $response = $client->getDocument($guide['facto_document_id'], true);
        $parser = new FactoDocumentResponseParser(is_array($response) ? $response : array());
        $parser->assertSuccess('reparación local de guía');
        $this->validateIssuedGuide($guide, $parser);
        $saved = $this->storage->saveElectronicDocumentFiles(
            'phase8_2_guide_issued_' . (int)$guide['id'] . '_' . $guide['facto_document_id'],
            $parser->electronicDocument(),
            $guide['environment']
        );
        $this->finalizeLocalIssue(
            (int)$guide['id'],
            $payload,
            $response,
            $guide['facto_document_id'],
            $parser->documentNumber(),
            $saved,
            $userId
        );
        return $this->guideResult($this->getGuide((int)$guide['id']));
    }

    public function getGuide($guideId)
    {
        $stmt = $this->pdo->prepare("SELECT * FROM billing_dispatch_guides WHERE id=? LIMIT 1");
        $stmt->execute(array((int)$guideId));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            throw new RuntimeException('Guía de despacho no encontrada.');
        }
        return $row;
    }

    private function prepareContext(array $data, $environment)
    {
        $sourceType = $this->normalizeSourceType($data['source_type'] ?? 'quote');
        $sourceId = (int)($data['source_id'] ?? 0);
        $source = $this->getSource($sourceType, $sourceId, $environment);
        $type = $this->types->getIssuedType('dispatch_guide_electronic', $environment);
        if (empty($type['environment_enabled'])) {
            throw new RuntimeException('La guía de despacho no está habilitada en ' . $environment . '.');
        }

        $issueDate = $this->normalizeDate($data['issue_date'] ?? date('Y-m-d'));
        if (!$issueDate) {
            throw new InvalidArgumentException('La fecha de emisión no es válida.');
        }
        $guideType = (int)($data['guide_type'] ?? 1);
        if ($guideType < 1 || $guideType > 9) {
            throw new InvalidArgumentException('Tipo de guía inválido.');
        }
        $transferType = $guideType === 5 ? null : (int)($data['transfer_type'] ?? 0);
        if ($guideType !== 5 && !in_array($transferType, array(1,2,3), true)) {
            throw new InvalidArgumentException('Debes seleccionar el tipo de traslado.');
        }

        $destination = array(
            'address' => trim((string)($data['dispatch_address'] ?? $source['customer']['address'])),
            'district' => trim((string)($data['dispatch_district'] ?? $source['customer']['district'])),
            'city' => trim((string)($data['dispatch_city'] ?? $source['customer']['city']))
        );
        if ($destination['address'] === '' || $destination['district'] === '' || $destination['city'] === '') {
            throw new InvalidArgumentException('Dirección, comuna y ciudad de destino son obligatorias.');
        }

        $companySettings = $this->loadCompanySettings();
        $factoConfig = icylab_facto_config($environment);
        $company = array(
            'tax_id' => $companySettings['company_tax_id'] ?? '',
            'legal_name' => $companySettings['company_name'] ?? '',
            'address' => $companySettings['company_address'] ?? '',
            'district' => $companySettings['company_comuna'] ?? '',
            'city' => $companySettings['company_ciudad'] ?? '',
            'phone' => $companySettings['company_phone'] ?? '',
            'activity' => $companySettings['company_giro'] ?? ''
        );
        $receiver = $source['customer'];
        $receiver['address'] = $destination['address'];
        $receiver['district'] = $destination['district'];
        $receiver['city'] = $destination['city'];

        $this->validateParty($company, 'emisor');
        $this->validateParty($receiver, 'receptor');
        if ($guideType === 5 && $this->normalizeTaxId($receiver['tax_id']) !== $this->normalizeTaxId($company['tax_id'])) {
            throw new RuntimeException('El traslado interno sólo puede usarse cuando emisor y receptor corresponden a la misma empresa.');
        }

        $requestedLines = json_decode((string)($data['lines_json'] ?? '[]'), true);
        if (!is_array($requestedLines)) {
            throw new InvalidArgumentException('El detalle de líneas no es válido.');
        }
        $lines = $this->calculateSelectedLines($source['lines'], $requestedLines);
        if (!$lines) {
            throw new InvalidArgumentException('Debes incluir al menos una línea con cantidad mayor a cero.');
        }

        $totals = array('net_amount' => 0, 'taxes_amount' => 0, 'total_amount' => 0);
        foreach ($lines as $line) {
            $totals['net_amount'] += (int)$line['net_amount'];
            $totals['taxes_amount'] += (int)$line['tax_amount'];
            $totals['total_amount'] += (int)$line['total_amount'];
        }

        $observations = trim((string)($data['observations'] ?? ''));
        $sourceLabel = $sourceType === 'invoice' ? 'Factura ' . $source['number'] : 'Cotización ' . $source['number'];
        $header = array(
            'account_id' => (int)($factoConfig['account_id'] ?? 0),
            'document_type_id' => 54,
            'received_issued_flag' => 1,
            'issue_date' => $issueDate,
            'issuer_tax_id_code' => billing_clean_rut($company['tax_id']),
            'issuer_tax_id_type' => 'CL-RUT',
            'issuer_legal_name' => $company['legal_name'],
            'issuer_address' => $company['address'],
            'issuer_district' => $company['district'],
            'issuer_city' => $company['city'],
            'issuer_country_id' => 253,
            'issuer_phone' => $company['phone'],
            'issuer_activity' => $company['activity'],
            'receiver_tax_id_code' => billing_clean_rut($receiver['tax_id']),
            'receiver_tax_id_type' => 'CL-RUT',
            'receiver_legal_name' => $receiver['legal_name'],
            'receiver_address' => $receiver['address'],
            'receiver_district' => $receiver['district'],
            'receiver_city' => $receiver['city'],
            'receiver_country_id' => 253,
            'receiver_phone' => $receiver['phone'],
            'receiver_activity' => $receiver['activity'],
            'payment_conditions' => '0',
            'currency_id' => 39,
            'guide_type' => $guideType,
            'observations' => billing_safe_substr('ICYLAB OS - ' . $sourceLabel . ($observations !== '' ? ' - ' . $observations : ''), 250),
            'deployment_observations' => 1
        );
        if ($transferType !== null) {
            $header['transfer_type'] = $transferType;
        }
        if (!empty($receiver['email'])) {
            $header['receiver_email'] = $receiver['email'];
        }

        $details = array();
        foreach ($lines as $line) {
            $detail = array(
                'quantity' => (float)$line['dispatch_quantity'],
                'sku' => $line['sku'] ?: 'SIN-SKU',
                'line_description' => billing_safe_substr($line['description'], 100),
                'long_description' => billing_safe_substr($line['long_description'], 500),
                'unit_measure' => billing_normalize_unit_measure($line['unit_measure']),
                'unit_price' => (int)$line['unit_price'],
                'modifier_amount' => 0,
                'modifier_percentage' => 0,
                'total_taxes' => (int)$line['tax_amount'],
                'total_amount_line' => (int)$line['total_amount']
            );
            if ($line['tax_treatment'] === 'afecto' && (int)$line['tax_amount'] > 0) {
                $detail['taxes'] = array(array(
                    'tax_type_id' => defined('ICYLAB_FACTO_CHILE_IVA_TAX_TYPE_ID') ? (string)ICYLAB_FACTO_CHILE_IVA_TAX_TYPE_ID : '387',
                    'tax_percentage' => 19,
                    'tax_amount' => (int)$line['tax_amount']
                ));
            }
            $details[] = $detail;
        }

        $payload = array(
            'options' => array('rounding_type' => 'net'),
            'header' => $header,
            'details' => $details,
            'totals' => array(
                'net_amount' => (int)$totals['net_amount'],
                'taxes_amount' => (int)$totals['taxes_amount'],
                'total_amount' => (int)$totals['total_amount']
            )
        );

        return array(
            'environment' => $environment,
            'source_type' => $sourceType,
            'source' => $source,
            'source_summary' => array(
                'type' => $sourceType,
                'id' => (int)$source['id'],
                'number' => $source['number'],
                'customer' => $receiver['legal_name'],
                'tax_id' => $receiver['tax_id']
            ),
            'issue_date' => $issueDate,
            'guide_type' => $guideType,
            'transfer_type' => $transferType,
            'destination' => $destination,
            'observations' => $observations,
            'lines' => $lines,
            'totals' => $totals,
            'payload' => $payload,
            'idempotency_key' => $this->makeIdempotencyKey($environment, $sourceType, $sourceId, $issueDate, $guideType, $transferType, $destination, $lines)
        );
    }

    private function createOrResetGuide(array $context, array $previewPayload, array $issuePayload, $userId)
    {
        $this->pdo->beginTransaction();
        try {
            $stmt = $this->pdo->prepare("SELECT * FROM billing_dispatch_guides WHERE idempotency_key=? FOR UPDATE");
            $stmt->execute(array($context['idempotency_key']));
            $existing = $stmt->fetch(PDO::FETCH_ASSOC);
            if ($existing && $existing['status'] === 'issued') {
                throw new RuntimeException('Esta guía ya fue emitida.');
            }
            if ($existing && $existing['status'] === 'remote_uncertain') {
                throw new RuntimeException('Esta guía tiene resultado remoto incierto y no puede reenviarse.');
            }
            if ($existing && $existing['status'] === 'pending_local_commit') {
                throw new RuntimeException('La guía ya fue identificada en FACTO y requiere reparar el cierre local; no debe emitirse nuevamente.');
            }
            if ($existing && $existing['status'] === 'issuing') {
                throw new RuntimeException('La guía está en proceso de emisión. Actualiza antes de reintentar.');
            }

            $source = $context['source'];
            if ($existing) {
                $guideId = (int)$existing['id'];
                $update = $this->pdo->prepare("
                    UPDATE billing_dispatch_guides
                    SET status='draft',issue_date=?,guide_type=?,transfer_type=?,dispatch_address=?,dispatch_district=?,dispatch_city=?,
                        observations=?,preview_request_payload=?,preview_response_payload=NULL,preview_facto_document_id=NULL,
                        preview_document_number=NULL,preview_pdf_file_path=NULL,preview_xml_file_path=NULL,preview_transfer_copy_pdf_file_path=NULL,
                        issue_request_payload=?,issue_response_payload=NULL,facto_document_id=NULL,document_number=NULL,
                        pdf_file_path=NULL,xml_file_path=NULL,transfer_copy_pdf_file_path=NULL,error_code=NULL,error_message=NULL,updated_at=NOW()
                    WHERE id=?
                ");
                $update->execute(array(
                    $context['issue_date'],$context['guide_type'],$context['transfer_type'],$context['destination']['address'],
                    $context['destination']['district'],$context['destination']['city'],$context['observations'],
                    $this->jsonEncode($previewPayload),$this->jsonEncode($issuePayload),$guideId
                ));
            } else {
                $insert = $this->pdo->prepare("
                    INSERT INTO billing_dispatch_guides
                    (environment,source_type,source_document_local_id,source_document_number,quote_id,opportunity_id,customer_id,
                     related_invoice_facto_document_id,issue_date,guide_type,transfer_type,dispatch_address,dispatch_district,dispatch_city,
                     observations,idempotency_key,preview_request_payload,issue_request_payload,stock_effect,status,created_by,created_at,updated_at)
                    VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'none','draft',?,NOW(),NOW())
                ");
                $insert->execute(array(
                    $context['environment'],$context['source_type'],(int)$source['id'],$source['number'],
                    $source['quote_id'] ?: null,$source['opportunity_id'] ?: null,$source['customer_id'] ?: null,
                    $source['invoice_facto_document_id'] ?: null,$context['issue_date'],$context['guide_type'],$context['transfer_type'],
                    $context['destination']['address'],$context['destination']['district'],$context['destination']['city'],
                    $context['observations'],$context['idempotency_key'],$this->jsonEncode($previewPayload),
                    $this->jsonEncode($issuePayload),$userId ?: null
                ));
                $guideId = (int)$this->pdo->lastInsertId();
            }

            $this->pdo->prepare("DELETE FROM billing_dispatch_guide_lines WHERE dispatch_guide_id=?")->execute(array($guideId));
            $lineInsert = $this->pdo->prepare("
                INSERT INTO billing_dispatch_guide_lines
                (dispatch_guide_id,source_line_id,product_id,sku,description,long_description,source_quantity,
                 dispatch_quantity,unit_measure,unit_price,net_amount,tax_amount,total_amount,tax_treatment,raw_payload,created_at,updated_at)
                VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),NOW())
            ");
            foreach ($context['lines'] as $line) {
                $lineInsert->execute(array(
                    $guideId,$line['source_line_id'],$line['product_id'],$line['sku'],$line['description'],$line['long_description'],
                    $line['source_quantity'],$line['dispatch_quantity'],$line['unit_measure'],$line['unit_price'],$line['net_amount'],
                    $line['tax_amount'],$line['total_amount'],$line['tax_treatment'],$this->jsonEncode($line)
                ));
            }
            $this->pdo->commit();
            return $this->getGuide($guideId);
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $e;
        }
    }

    private function loadInvoiceSource($id, $environment)
    {
        $stmt = $this->pdo->prepare("
            SELECT fd.*,q.quote_number,q.opportunity_id,q.finance_center_id AS quote_finance_center_id,
                   c.customer_name,c.legal_name,c.customer_tax_id,c.address,c.district,c.city,c.phone,c.email,c.email_dte,c.business_activity
            FROM facto_documents fd
            LEFT JOIN crm_quotes q ON q.id=fd.crm_quote_id
            LEFT JOIN crm_customers c ON c.id=fd.crm_customer_id
            WHERE fd.id=? AND fd.environment=? AND CAST(fd.document_type_id AS UNSIGNED) IN (2,32) LIMIT 1
        ");
        $stmt->execute(array($id, $environment));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            throw new RuntimeException('Factura de origen no encontrada.');
        }
        $lineStmt = $this->pdo->prepare("SELECT * FROM billing_document_lines WHERE environment=? AND facto_document_id=? ORDER BY id");
        $lineStmt->execute(array($environment, $row['document_id']));
        $rawLines = $lineStmt->fetchAll(PDO::FETCH_ASSOC);
        if (!$rawLines && !empty($row['crm_quote_id'])) {
            $rawLines = $this->loadQuoteLines((int)$row['crm_quote_id']);
        }
        $lines = $this->applyPreviouslyDispatched('invoice', (int)$row['id'], $this->normalizeSourceLines($rawLines, 'invoice'));
        if (!$lines) {
            throw new RuntimeException('La factura no tiene líneas locales disponibles para crear la guía.');
        }
        return array(
            'type' => 'invoice','id' => (int)$row['id'],'number' => $row['document_number'],'date' => $row['issue_date'],
            'quote_id' => $row['crm_quote_id'] ? (int)$row['crm_quote_id'] : null,
            'opportunity_id' => $row['crm_opportunity_id'] ? (int)$row['crm_opportunity_id'] : ($row['opportunity_id'] ? (int)$row['opportunity_id'] : null),
            'customer_id' => $row['crm_customer_id'] ? (int)$row['crm_customer_id'] : null,
            'invoice_facto_document_id' => $row['document_id'],
            'finance_center_id' => $row['finance_center_id'] ?: $row['quote_finance_center_id'],
            'customer' => $this->customerFromRow($row),
            'lines' => $lines,
            'total_amount' => billing_money($row['total_amount'])
        );
    }

    private function loadQuoteSource($id, $environment)
    {
        $stmt = $this->pdo->prepare("
            SELECT q.*,c.customer_name,c.legal_name,c.customer_tax_id,c.address,c.district,c.city,c.phone,c.email,c.email_dte,c.business_activity
            FROM crm_quotes q INNER JOIN crm_customers c ON c.id=q.customer_id
            WHERE q.id=? AND q.deleted_at IS NULL LIMIT 1
        ");
        $stmt->execute(array($id));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            throw new RuntimeException('Cotización de origen no encontrada.');
        }
        if (!in_array(strtolower(trim((string)$row['status'])), array('aceptada','pagada','facturada'), true)) {
            throw new RuntimeException('La cotización debe estar Aceptada, Pagada o Facturada para crear una guía.');
        }
        $lines = $this->applyPreviouslyDispatched('quote', (int)$row['id'], $this->normalizeSourceLines($this->loadQuoteLines($id), 'quote'));
        if (!$lines) {
            throw new RuntimeException('La cotización no tiene líneas disponibles.');
        }
        return array(
            'type' => 'quote','id' => (int)$row['id'],'number' => $row['quote_number'],'date' => substr((string)$row['created_at'],0,10),
            'quote_id' => (int)$row['id'],'opportunity_id' => $row['opportunity_id'] ? (int)$row['opportunity_id'] : null,
            'customer_id' => (int)$row['customer_id'],'invoice_facto_document_id' => $row['facto_document_id'] ?: null,
            'finance_center_id' => $row['finance_center_id'] ?: null,'customer' => $this->customerFromRow($row),
            'lines' => $lines,'total_amount' => billing_money($row['total'])
        );
    }

    private function loadQuoteLines($quoteId)
    {
        $stmt = $this->pdo->prepare("
            SELECT qi.*,p.sku AS product_sku,p.dte_unit_code,p.name AS product_name,p.description AS product_description
            FROM crm_quote_items qi
            LEFT JOIN products p ON p.id=CAST(qi.product_id AS UNSIGNED)
            WHERE qi.quote_id=? ORDER BY qi.id
        " );
        $stmt->execute(array((int)$quoteId));
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function normalizeSourceLines(array $rows, $sourceType)
    {
        $lines = array();
        foreach ($rows as $row) {
            $qty = (float)($row['quantity'] ?? 0);
            if ($qty <= 0) continue;
            $net = billing_money($row['net_amount'] ?? 0);
            $tax = billing_money($row['tax_amount'] ?? 0);
            $total = billing_money($row['gross_amount'] ?? ($row['total_amount'] ?? ($row['total_item'] ?? 0)));
            if ($net <= 0) {
                $unit = billing_money($row['price_unit'] ?? ($row['unit_price'] ?? 0));
                $discount = billing_money($row['discount_amount'] ?? 0);
                $net = max(0, billing_money($unit * $qty - $discount));
            }
            $taxTreatment = (string)($row['tax_treatment'] ?? ($tax > 0 ? 'afecto' : 'exento'));
            if ($tax <= 0 && $taxTreatment === 'afecto') $tax = round($net * 0.19, 0);
            if ($total <= 0) $total = $net + $tax;
            $unitPrice = $qty > 0 ? round($net / $qty, 0) : 0;
            $description = trim((string)($row['name'] ?? ($row['description'] ?? 'Producto')));
            $longDescription = trim((string)($row['description'] ?? $description));
            $lines[] = array(
                'source_line_id' => (string)($row['id'] ?? ''),
                'product_id' => (string)($row['product_id'] ?? ''),
                'sku' => trim((string)($row['sku'] ?? ($row['product_sku'] ?? billing_extract_sku($description, $longDescription)))),
                'description' => $description,
                'long_description' => $longDescription,
                'source_quantity' => $qty,
                'unit_measure' => billing_normalize_unit_measure($row['dte_unit_code'] ?? ($row['unit'] ?? ($row['unit_measure'] ?? 'UN'))),
                'unit_price' => $unitPrice,
                'source_net_amount' => $net,
                'source_tax_amount' => $tax,
                'source_total_amount' => $total,
                'tax_treatment' => $taxTreatment === 'exento' ? 'exento' : 'afecto'
            );
        }
        return $lines;
    }

    private function calculateSelectedLines(array $sourceLines, array $requestedLines)
    {
        $map = array();
        foreach ($sourceLines as $line) $map[(string)$line['source_line_id']] = $line;
        $selected = array();
        foreach ($requestedLines as $requested) {
            $id = (string)($requested['source_line_id'] ?? '');
            $qty = (float)($requested['quantity'] ?? 0);
            if ($qty <= 0) continue;
            if (!isset($map[$id])) throw new RuntimeException('Una línea seleccionada ya no existe en el origen.');
            $source = $map[$id];
            if ($qty > (float)$source['available_quantity'] + 0.0001) {
                throw new RuntimeException('La cantidad despachada de ' . $source['description'] . ' supera la cantidad pendiente disponible.');
            }
            $net = (int)round((float)$source['unit_price'] * $qty, 0);
            $tax = $source['tax_treatment'] === 'afecto' ? (int)round($net * 0.19, 0) : 0;
            $total = $net + $tax;
            $source['dispatch_quantity'] = $qty;
            $source['net_amount'] = $net;
            $source['tax_amount'] = $tax;
            $source['total_amount'] = $total;
            $selected[] = $source;
        }
        return $selected;
    }

    private function applyPreviouslyDispatched($sourceType, $sourceId, array $lines)
    {
        if (!$lines) return $lines;
        $stmt = $this->pdo->prepare("
            SELECT l.source_line_id,COALESCE(SUM(l.dispatch_quantity),0) AS dispatched_quantity
            FROM billing_dispatch_guide_lines l
            INNER JOIN billing_dispatch_guides g ON g.id=l.dispatch_guide_id
            WHERE g.source_type=? AND g.source_document_local_id=? AND g.status='issued'
            GROUP BY l.source_line_id
        ");
        $stmt->execute(array($sourceType, (int)$sourceId));
        $used = array();
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $used[(string)$row['source_line_id']] = (float)$row['dispatched_quantity'];
        }
        foreach ($lines as &$line) {
            $original = (float)$line['source_quantity'];
            $dispatched = (float)($used[(string)$line['source_line_id']] ?? 0);
            $line['original_quantity'] = $original;
            $line['dispatched_quantity'] = $dispatched;
            $line['available_quantity'] = max(0, $original - $dispatched);
        }
        unset($line);
        return $lines;
    }

    private function customerFromRow(array $row)
    {
        return array(
            'tax_id' => $row['customer_tax_id'] ?: ($row['receiver_tax_id_code'] ?? ''),
            'legal_name' => $row['legal_name'] ?: ($row['customer_name'] ?: ($row['receiver_legal_name'] ?? '')),
            'address' => trim((string)($row['address'] ?? '')),
            'district' => trim((string)($row['district'] ?? '')),
            'city' => trim((string)($row['city'] ?? '')),
            'phone' => trim((string)($row['phone'] ?? '')),
            'email' => trim((string)($row['email_dte'] ?: ($row['email'] ?? ''))),
            'activity' => trim((string)($row['business_activity'] ?? ''))
        );
    }

    private function validateParty(array $party, $label)
    {
        $rut = billing_clean_rut($party['tax_id'] ?? '');
        if (!$this->factory->isValidChileRut($rut)) {
            throw new RuntimeException('El RUT del ' . $label . ' no es válido: ' . $rut . '.');
        }
        $missing = array();
        foreach (array('legal_name'=>'razón social','address'=>'dirección','district'=>'comuna','city'=>'ciudad','activity'=>'giro') as $key=>$name) {
            if (trim((string)($party[$key] ?? '')) === '') $missing[] = $name;
        }
        if ($missing) throw new RuntimeException('Faltan datos del ' . $label . ': ' . implode(', ', $missing) . '.');
    }

    private function assertSourceStillValid(array $guide)
    {
        $source = $this->getSource($guide['source_type'], (int)$guide['source_document_local_id'], $guide['environment']);
        $lineStmt = $this->pdo->prepare("SELECT source_line_id,dispatch_quantity FROM billing_dispatch_guide_lines WHERE dispatch_guide_id=?");
        $lineStmt->execute(array((int)$guide['id']));
        $current = array(); foreach ($source['lines'] as $line) $current[(string)$line['source_line_id']] = (float)$line['available_quantity'];
        foreach ($lineStmt->fetchAll(PDO::FETCH_ASSOC) as $line) {
            $id = (string)$line['source_line_id'];
            if (!isset($current[$id]) || (float)$line['dispatch_quantity'] > $current[$id] + 0.0001) {
                throw new RuntimeException('El origen cambió después del preview. Vuelve a preparar la guía.');
            }
        }
    }

    private function validateIssuedGuide(array $guide, FactoDocumentResponseParser $parser)
    {
        $header = $parser->header();
        $type = (int)($header['document_type_id'] ?? 0);
        if ($type && $type !== 54) throw new RuntimeException('El documento remoto no corresponde a una guía de despacho.');
        $receiver = $this->normalizeTaxId($header['receiver_tax_id_code'] ?? '');
        $expected = $this->normalizeTaxId($this->guideReceiverTaxId($guide));
        if ($receiver !== '' && $expected !== '' && $receiver !== $expected) {
            throw new RuntimeException('El receptor de la guía remota no coincide con el origen.');
        }
        $totals = $parser->totals();
        if (isset($totals['total_amount'])) {
            $expectedTotal = $this->guideTotal($guide['id']);
            if (abs((float)$totals['total_amount'] - $expectedTotal) > 1) {
                throw new RuntimeException('El total de la guía remota no coincide con el preview.');
            }
        }
    }

    private function finalizeLocalIssue($guideId, array $payload, array $response, $factoDocumentId, $documentNumber, array $saved, $userId)
    {
        $this->pdo->beginTransaction();
        try {
            $guide = $this->getGuideForUpdate($guideId);
            $this->persistIssuedDocument($guide, $payload, $response, $factoDocumentId, $documentNumber, $saved, $userId);
            $stmt = $this->pdo->prepare("
                UPDATE billing_dispatch_guides
                SET status='issued',facto_document_id=?,document_number=?,pdf_file_path=?,xml_file_path=?,transfer_copy_pdf_file_path=?,
                    issue_response_payload=?,error_code=NULL,error_message=NULL,issued_by=?,issued_at=NOW(),updated_at=NOW()
                WHERE id=?
            ");
            $stmt->execute(array(
                $factoDocumentId,$documentNumber,$saved['pdf_file_path'] ?? null,$saved['xml_file_path'] ?? null,
                $saved['transfer_copy_pdf_file_path'] ?? null,
                $this->jsonEncode((new FactoDocumentResponseParser($response))->safeResponse()),$userId ?: null,$guideId
            ));
            $this->pdo->commit();
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $e;
        }
    }

    private function persistIssuedDocument(array $guide, array $payload, array $response, $factoDocumentId, $documentNumber, array $saved, $userId)
    {
        $header = $payload['header']; $totals = $payload['totals'];
        $raw = $this->jsonEncode((new FactoDocumentResponseParser($response))->safeResponse());
        $hash = hash('sha256', $this->jsonEncode($payload));
        $stmt = $this->pdo->prepare("
            INSERT INTO facto_documents
            (environment,document_id,account_id,issue_date,document_number,document_type_id,issuer_tax_id_code,
             receiver_legal_name,receiver_tax_id_code,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,document_type_taxbureau,pdf_file_path,xml_file_path,
             raw_payload,payload_hash,last_synced_at,created_at,updated_at)
            VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,'manual_linked',100,?,NOW(),1,39,1,?,?,?,'no_facturado','52',?,?,?,?,NOW(),NOW(),NOW())
            ON DUPLICATE KEY UPDATE
              document_number=VALUES(document_number),issue_date=VALUES(issue_date),crm_customer_id=VALUES(crm_customer_id),
              crm_quote_id=VALUES(crm_quote_id),crm_opportunity_id=VALUES(crm_opportunity_id),finance_center_id=VALUES(finance_center_id),
              net_amount=VALUES(net_amount),taxes_amount=VALUES(taxes_amount),total_amount=VALUES(total_amount),
              pdf_file_path=VALUES(pdf_file_path),xml_file_path=VALUES(xml_file_path),raw_payload=VALUES(raw_payload),
              payload_hash=VALUES(payload_hash),last_synced_at=NOW(),updated_at=NOW()
        ");
        $stmt->execute(array(
            $guide['environment'],$factoDocumentId,$header['account_id'],$guide['issue_date'],$documentNumber,54,
            $header['issuer_tax_id_code'],$header['receiver_legal_name'],$header['receiver_tax_id_code'],
            $guide['customer_id'] ?: null,$guide['quote_id'] ?: null,$guide['opportunity_id'] ?: null,$this->sourceFinanceCenter($guide),
            $userId ?: null,$totals['net_amount'],$totals['taxes_amount'],$totals['total_amount'],
            $saved['pdf_file_path'] ?? null,$saved['xml_file_path'] ?? null,$raw,$hash
        ));

        $this->pdo->prepare("DELETE FROM billing_document_lines WHERE environment=? AND facto_document_id=?")
            ->execute(array($guide['environment'],$factoDocumentId));
        $lineStmt = $this->pdo->prepare("SELECT * FROM billing_dispatch_guide_lines WHERE dispatch_guide_id=? ORDER BY id");
        $lineStmt->execute(array((int)$guide['id']));
        $insert = $this->pdo->prepare("
            INSERT INTO billing_document_lines
            (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,created_at)
            VALUES (?,?,?,?,?,?,?,?,?,?,0,0,?,?,?,?,?,?,NOW())
        ");
        foreach ($lineStmt->fetchAll(PDO::FETCH_ASSOC) as $line) {
            $productId = ctype_digit((string)$line['product_id']) ? (int)$line['product_id'] : null;
            $insert->execute(array(
                $guide['environment'],$factoDocumentId,$guide['quote_id'] ?: null,$line['source_line_id'],$productId,
                $line['sku'],$line['description'],$line['dispatch_quantity'],$line['unit_measure'],$line['unit_price'],
                $line['net_amount'],$line['tax_amount'],$line['total_amount'],$line['tax_treatment'],$this->sourceFinanceCenter($guide),$line['raw_payload']
            ));
        }
    }

    private function getGuideForUpdate($guideId)
    {
        $stmt = $this->pdo->prepare("SELECT * FROM billing_dispatch_guides WHERE id=? FOR UPDATE");
        $stmt->execute(array((int)$guideId));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) throw new RuntimeException('Guía de despacho no encontrada.');
        return $row;
    }

    private function guideResult(array $guide)
    {
        return array(
            'guide_id' => (int)$guide['id'],'status' => $guide['status'],'facto_document_id' => $guide['facto_document_id'],
            'document_number' => $guide['document_number'],'source_type' => $guide['source_type'],
            'source_number' => $guide['source_document_number'],'pdf_available' => !empty($guide['pdf_file_path']),
            'xml_available' => !empty($guide['xml_file_path']),
            'transfer_copy_pdf_available' => !empty($guide['transfer_copy_pdf_file_path']),
            'error_message' => $guide['error_message']
        );
    }

    private function guideReceiverTaxId(array $guide)
    {
        $source = $this->getSource($guide['source_type'], (int)$guide['source_document_local_id'], $guide['environment']);
        return $source['customer']['tax_id'];
    }

    private function guideTotal($guideId)
    {
        $stmt = $this->pdo->prepare("SELECT COALESCE(SUM(total_amount),0) FROM billing_dispatch_guide_lines WHERE dispatch_guide_id=?");
        $stmt->execute(array((int)$guideId));
        return (float)$stmt->fetchColumn();
    }

    private function sourceFinanceCenter(array $guide)
    {
        if (!empty($guide['quote_id'])) {
            $stmt = $this->pdo->prepare("SELECT finance_center_id FROM crm_quotes WHERE id=? LIMIT 1");
            $stmt->execute(array((int)$guide['quote_id']));
            return $stmt->fetchColumn() ?: null;
        }
        return null;
    }

    private function markError($guideId, $code, $message, $response = null, $phase = 'issue')
    {
        $column = $phase === 'preview' ? 'preview_response_payload' : 'issue_response_payload';
        $safe = is_array($response) ? $this->jsonEncode((new FactoDocumentResponseParser($response))->safeResponse()) : null;
        $stmt = $this->pdo->prepare("UPDATE billing_dispatch_guides SET status='error',{$column}=?,error_code=?,error_message=?,updated_at=NOW() WHERE id=?");
        $stmt->execute(array($safe,$code,$message,(int)$guideId));
    }

    private function markPendingLocalCommit($guideId, $documentId, $response, $message)
    {
        $safe = is_array($response) ? $this->jsonEncode((new FactoDocumentResponseParser($response))->safeResponse()) : null;
        $stmt = $this->pdo->prepare("UPDATE billing_dispatch_guides SET status='pending_local_commit',facto_document_id=?,issue_response_payload=?,error_code='local_commit_error',error_message=?,updated_at=NOW() WHERE id=?");
        $stmt->execute(array($documentId,$safe,$message,(int)$guideId));
    }

    private function markRemoteUncertain($guideId, $response, $message)
    {
        $safe = is_array($response) ? $this->jsonEncode((new FactoDocumentResponseParser($response))->safeResponse()) : null;
        $stmt = $this->pdo->prepare("UPDATE billing_dispatch_guides SET status='remote_uncertain',issue_response_payload=?,error_code='missing_document_id',error_message=?,updated_at=NOW() WHERE id=?");
        $stmt->execute(array($safe,$message,(int)$guideId));
    }

    private function makeIdempotencyKey($environment, $sourceType, $sourceId, $issueDate, $guideType, $transferType, array $destination, array $lines)
    {
        $lineKey = array();
        foreach ($lines as $line) $lineKey[] = $line['source_line_id'] . ':' . number_format((float)$line['dispatch_quantity'],4,'.','');
        return 'phase8.2:' . $environment . ':' . $sourceType . ':' . (int)$sourceId . ':' . $issueDate
            . ':g' . (int)$guideType . ':t' . (int)$transferType . ':'
            . substr(hash('sha256', implode('|',$lineKey) . '|' . implode('|',$destination)),0,32);
    }

    private function loadCompanySettings()
    {
        $stmt = $this->pdo->query("SELECT * FROM system_settings WHERE id=1 LIMIT 1");
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: array();
    }

    private function guideTypeLabel($type)
    {
        $map = array(1=>'Operación constituye venta',2=>'Ventas por efectuar',3=>'Consignación',4=>'Entrega gratuita',5=>'Traslado interno',6=>'Otros traslados no venta',7=>'Devolución',8=>'Traslado para exportación',9=>'Venta para exportación');
        return $map[(int)$type] ?? 'Guía de despacho';
    }

    private function transferTypeLabel($type)
    {
        $map = array(1=>'Despacho por cuenta del receptor',2=>'Despacho por cuenta del emisor a instalaciones del receptor',3=>'Despacho por cuenta del emisor a otras instalaciones');
        return $type === null ? 'No aplica' : ($map[(int)$type] ?? 'No definido');
    }

    private function normalizeSourceType($sourceType)
    {
        $sourceType = strtolower(trim((string)$sourceType));
        if (!in_array($sourceType,array('quote','invoice'),true)) throw new InvalidArgumentException('Tipo de origen inválido.');
        return $sourceType;
    }

    private function normalizeEnvironment($environment)
    {
        return billing_normalize_environment($environment);
    }

    private function normalizeDate($date)
    {
        $date = trim((string)$date); if ($date==='') return null;
        $parsed = DateTime::createFromFormat('Y-m-d',substr($date,0,10));
        return $parsed && $parsed->format('Y-m-d')===substr($date,0,10) ? $parsed->format('Y-m-d') : null;
    }

    private function normalizeTaxId($value)
    {
        return strtoupper(preg_replace('/[^0-9K]/','',(string)$value));
    }

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