<?php

namespace App\Services;

use App\Data\DiscountData;
use App\Data\ElectronicDocument;
use App\Data\LineaItemData;
use App\Data\TaxData;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

use App\HttpClients\Clients\HaciendaClient;
use App\Services\CertificadosService;
use App\Utils\ConsecutiveGenerator;

use App\Factories\ElectronicDocumentFactory;

use App\Enums\CodigoImpuesto;
use App\Enums\CodigoTarifaIVA;
use App\Enums\TipoDocumento;
use App\HttpClients\DTOs\Hacienda\DocStatusResponse;
use App\Models\Sucursal;
use App\Utils\SucursalUtil;
use Carbon\Carbon;

use Closure;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\UnauthorizedException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;

use Illuminate\Support\Facades\Http;

use App\Services\ElectronicBilling\Builders\MensajeReceptorBuilder;

class DocumentoService 
{
    protected $client;

    public function __construct(
        protected ElectronicDocumentFactory $factory,
        protected HaciendaClient $mh_client,
        protected TipoCambioService $tc_client,
        protected HaciendaSigner $signer
    ) {}

    public function agregarCliente($data) 
    {
        try {
            $response = Http::withOptions(['verify' => false])
                ->get('https://api.hacienda.go.cr/fe/ae', [
                    'identificacion' => $data['numero_identificacion']
                ])
                ->throw()
                ->json();

            DB::table('clientes')->insertGetId([
                'nombre' => $response['nombre'],
                'tipo_identificacion' => $response['tipoIdentificacion'],
                'identificacion' => $data['numero_identificacion'],
                'pin' => $data['pin']
            ]);
        } catch (Throwable $e) {
            return new Exception("No se ha podido agregar el cliente");
        }
    }

    public function crearSucursalApiKey($clienteId, $sucursalId) 
    {
        $exists = DB::table('sucursales')
            ->where('id', $sucursalId)
            ->where('cliente_id', $clienteId)
            ->exists();

        if (!$exists) {
            throw new Exception("No existe esta sucursal para crear un API-KEY");
        }

        $data = [
            "sucursal_id" => $sucursalId,
            "api_key" => SucursalUtil::generarApiKey()
        ];

        $id = DB::table(table: 'api_keys')->insertGetId($data);
        return DB::table('api_keys')->where('id', $id)->first(); 
    }

    public function getSucursalApiKeys($clienteId, $sucursalId)
    {
        $apiKeys = DB::table('api_keys')
            ->join('sucursales', 'api_keys.sucursal_id', '=', 'sucursales.id')
            ->where('sucursales.cliente_id', $clienteId)
            ->where('sucursales.id', $sucursalId)
            // ->select()
            ->get();
       

        return $apiKeys;
    }

    public function crearSucursal($data, $clienteId) 
    {
        $insertData = SucursalUtil::normalizeData($data);
        $insertData['cliente_id'] = $clienteId;

        $exists = DB::table('sucursales')
            ->where('cod_sucursal', $insertData['cod_sucursal'])
            ->exists();

        if ($exists) {
            throw new Exception("Ya existe una sucursal con el mismo codigo");
        }

        $id = DB::table('sucursales')->insertGetId($insertData); 
        return DB::table('sucursales')->where('id', $id)->first();
    }

    public function updateSucursal($data, $clienteId, $sucursalId) 
    {
        $allowed = [ 
            'correo', 
            'telefono', 
            'provincia', 
            'canton', 
            'distrito', 
            'sennas', 
            'nombre_comercial',
            'cod_actividad',
        ];

        $datos = SucursalUtil::normalizeData($data);

        $updateData = array_intersect_key($datos, array_flip($allowed));
        $updateData = array_filter($updateData, function ($value) {
            return !is_null($value) && $value !== '';
        });

        if (empty($updateData)) { 
            throw new Exception("No se recibieron datos para actualizar sucursal");
        }

        $updated = DB::table('sucursales')
            ->where('id', $sucursalId)
            ->where('cliente_id', $clienteId)
            ->update($updateData); 
        
        return DB::table('sucursales')
            ->where('id', $sucursalId)
            ->where('cliente_id', $clienteId)
            ->first(); 
    }

    public function deleteSucursal(int $sucursalId, int $clientId): bool 
    {
        $deleted = DB::table('sucursales')
            ->where('id', $sucursalId)
            ->where('cliente_id', $clientId) 
            ->delete();
        
        return $deleted > 0; 
    }

    public function getSucursalesByClient(int $clientId)
    {
        $sucursales = DB::table('sucursales')
            ->where('cliente_id', $clientId)
            ->get();

        return $sucursales;
    }
    
    public function procesarFirmados() 
    {
        $documentos = DB::table('documentos')
            ->where('estado', 'firmado')
            ->get();
        
        $resultados = []; 
        
        
        foreach ($documentos as $doc) {
            $sucursal = DB::table('sucursales') 
                ->where('id', $doc->sucursal_id) 
                ->first(); 
            
        
            
            if (!$sucursal) { continue; }
            
            $cliente = DB::table('clientes') 
                ->where('id', $sucursal->cliente_id) 
                ->first(); 
            
            $credenciales = DB::table('mh_credenciales')
                ->where('client_id', $sucursal->cliente_id) 
                ->first(); 
                
            if (!$credenciales) { continue; }
            
            $isSandbox = $credenciales->mh_environment == "sandbox";
            
            try {
                $result = [
                    "fecha" => $doc->fecha_emision,
                    "clave" => $doc->clave,
                    "comprobanteXml" => $doc->xml_firmado,
                    "emisor" => [
                        "tipo_identificacion" => $cliente->tipo_identificacion,
                        "numero_identificacion" => $cliente->identificacion
                    ]
                ];
                
                if (!empty($doc->receptor_ident) && !empty($doc->receptor_tipo_ident)) 
                {
                    $result['receptor'] = [
                        "tipo_identificacion" => $doc->receptor_tipo_ident,
                        "numero_identificacion" => $doc->receptor_ident
                    ];
                }
                
            
                
                $this->mh_client->useSandboxMode($isSandbox);
                $this->mh_client->authenticate($credenciales->mh_username, $credenciales->mh_password);
                
                $response = $this->mh_client->sendDocument($result);
                
                
    
                if ($response->accepted())
                {
                    DB::table('documentos')
                        ->where('clave', $result['clave'])
                        ->update([
                            'estado' => 'enviado'
                        ]);
                } else {
                    if ($response->hasHeader('X-Error-Cause')) {
                        $errorCause = $response->header('X-Error-Cause');
                    }
        
                    DB::table('documentos')
                        ->where('clave', $result['clave'])
                        ->update([
                            'estado' => 'error',
                            'respuesta_error_hacienda' => $errorCause ?? null
                        ]);
                }
                
            } catch (\Throwable $e) { continue; }
                
        }
        $this->procesarMensajesReceptorFirmados();
    }
    

    public function getDocsByClient(int $clienteId) 
    {
        $documents = DB::table('documentos')
            ->join('sucursales', 'documentos.sucursal_id', '=', 'sucursales.id')
            ->join('clientes', 'sucursales.cliente_id', '=', 'clientes.id')
            ->where('clientes.id', $clienteId)
            ->select(
                'documentos.clave',
                'documentos.consecutivo',
                'documentos.situacion',
                'documentos.receptor_tipo_ident',
                'documentos.receptor_ident',
                'documentos.receptor_nombre',
                'documentos.ambiente',
                'documentos.tipo_documento',
                'documentos.moneda',
                'documentos.xml_firmado',
                'documentos.tipo_cambio',
                'documentos.total_comprobante',
                'documentos.fecha_emision'
            )
            ->get();

        return $documents;
    }

    public function getDocumentByClave(string $clave)
    {
        $record = DB::table('documentos')
            ->where('clave', $clave)
            ->first();
        
        if (!$record) {
            throw new NotFoundHttpException("No se ha encontrado un documento para descargar");
        }

        return $record;
    }
    
    public function getDocumentLineasByDocId(string $id)
    {
        $record = DB::table('documento_detalles')
            ->where('documento_id', $id)
            ->get();

        return $record;
    }

    public function getCredentialsByEnvironment(int $clientId, string $environment) 
    {
        $record = DB::table('mh_credenciales')
            ->where('client_id', '=', $clientId)
            ->where('mh_environment', '=', $environment)
            ->first();

        return $record;
    }

    public function getDocumentStatus(string $clave, array $branchData): DocStatusResponse
    {
        $record = DB::table('documentos')
            ->where('clave', $clave)
            ->first();
    
        if ($record) {
            $document = DocStatusResponse::fromDatabase($record);
    
            if ($document->isAccepted()) {
                return $document;
            }
        }
    
        $this->testCredentials(
            $branchData['username'],
            $branchData['password'],
            (bool) $branchData['isSandbox']
        );
    
        $reception = $this->mh_client->getDocumentReceptionStatus($clave);
    
        if ($record) {
            DB::table('documentos')
                ->where('clave', $clave)
                ->whereIn('estado', ['enviado', 'pendiente', 'firmado'])
                ->update([
                    'estado' => $reception->response_xml ? 'finalizado' : 'pendiente',
                    'xml_respuesta' => $reception->response_xml ?? null,
                    'respuesta_hacienda' => $reception->status ?? null,
                ]);
        }
    
        return $reception;
    }

    public function addCredentials($data, $client_id) 
    {
        $sandboxMode = $data['mh_environment'] == "sandbox";
        $this->testCredentials($data['mh_username'], $data['mh_password'], $sandboxMode);

        $p12Content = file_get_contents($data['cert_file']->getRealPath());
        //$p12Processed = CertificadosService::procesarCertificado($p12Content, $data['cert_pin']);

        $certs = [];
        $isValid = openssl_pkcs12_read($p12Content, $certs, $data['cert_pin']);
        if (!$isValid) throw new Exception("No se ha podido procesar el certificado");

        $parsed = openssl_x509_parse($certs['cert']);
        $expiresAt = $parsed['validTo_time_t']; 

        $isExpired = $expiresAt < time();
        if ($isExpired) {
            throw new Exception('El certificado ya ha expirado');
        }

        if(!$parsed['subject']['serialNumber'] === strtoupper(strstr($data['mh_username'], '@', true))) 
        {
            throw new Exception("El certificado no corresponde al usuario brindado");
        }

        $cedula = preg_replace('/\D/', '', $parsed['subject']['serialNumber']);

        if (substr($cedula, 0, 1) === '0') {
            $cedula = substr($cedula, 1); 
        }

        $pathToSave = "certificados/{$cedula}/{$cedula}_{$data['mh_environment']}.p12";
        Storage::disk('local')->put($pathToSave, $p12Content);

        DB::table('mh_credenciales')->updateOrInsert([ 
            'client_id'         => $client_id,
            'mh_environment'    => $data['mh_environment']
        ], [
            'mh_username'       => $data['mh_username'], 
            'mh_password'       => $data['mh_password'],
            'cert_path'         => $pathToSave,
            'cert_pin'          => $data['cert_pin'],
        ]);
    }

    public function testCredentials(string $username, string $password, bool $isSandbox) 
    {
        $this->mh_client->useSandboxMode($isSandbox);
        return $this->mh_client->authenticate($username, $password);
    }

    public function addDocumento($branchData, $data) 
    {
        // SE HACE UNA PRE-FACTURA SIN LA CLAVE NUMERICA, SE INTENTA OBTENER EL TOKEN PARA
        // OBTENER LA SITUACION DEL DOCUMENTO, LUEGO SE ENTRA EN LA TRANSACCION, 
        // SE BLOQUEA LA SUCURSAL PARA OBTENER EL SIGUIENTE CONSECUTIVO
        // SE CREA EL XML CON LA CLAVE Y LA SITUACION...
        // Y SE INTENTA FIRMAR (SI NO ES VALIDA O NO EXISTE, SE TERMINA EL PROCESO)
        // SE GUARDA LO NECESARIO EN BASE DE DATOS... LUEGO DE LA TRANSACCION SE RETORNA
        // CLAVE, XML FIRMADO, SITUACION, Y SI FUE EXITOSO LA AUTH EN HACIENDA SE INTENTA ENVIAR
        // SINO SOLAMENTE SE RETORNA EXITOSO Y LUEGO SE INTENTA MEDIANTE CRON
   
        $tipoDocumento = TipoDocumento::from($data['tipo_documento']);
        $draftInvoice = $this->enrich([...$data, "branch" => $branchData]);
        
        $result = $this->withSiguienteNumero($branchData['sucursal_id'], $tipoDocumento->consecutivoCol(),
        function ($sucursal, $consecutivo) use ($data, $branchData, $draftInvoice)
        {
            
            try
            {
                $tipoDoc = $data['tipo_documento'];

                $consecutivoNumerico = ConsecutiveGenerator::generarNumeroConsecutivo(
                    $sucursal->cod_sucursal, 
                    $sucursal->cod_terminal, 
                    $tipoDoc, 
                    $consecutivo
                );

                $emisor = $data['emisor'];
                $now = Carbon::now('America/Costa_Rica');
                $situacion = "1";

                try {
                    $this->mh_client->useSandboxMode($branchData['isSandbox']);
                    $this->mh_client->authenticate($branchData['username'], $branchData['password']);
                } catch (\RuntimeException $e) {
                    // SI NO ES POSIBLE CONECTAR CON HACIENDA (NO RESPONDE EL SERVIDOR) SE HACE EL COMPROBANTE EN SITUACION 3
                    $situacion = "3";
                } catch (UnauthorizedException|RequestException $e) {
                    // SI NO TIENE CREDENCIALES VALIDAS SE RECHAZA LA CREACION DEL COMPROBANTE
                    throw new Exception('Credenciales incorrectas para conectar con Hacienda...');
                }
                
                $claveNumerica = ConsecutiveGenerator::generarClave(
                    $now,
                    $consecutivoNumerico, 
                    $emisor['tipo_identificacion'],
                    $emisor['numero_identificacion'],
                    $situacion
                );
                
                $finalInvoice = $draftInvoice->finalize(
                    $claveNumerica,
                    $consecutivoNumerico,
                    $now->toAtomString()
                );

                $xmlBuilder = $this->factory::create($tipoDoc);
                $rawXml     = $xmlBuilder->build($finalInvoice);

                $signedXml  = $this->signer::sign($branchData, $rawXml);

                $receptor = $draftInvoice->receptor ? [
                    'receptor_nombre' => $draftInvoice->receptor->nombre,
                    'receptor_nombre_comercial' => $draftInvoice->receptor->nombre_comercial,
                    'receptor_tipo_ident' => $draftInvoice->receptor->tipo_identificacion,
                    'receptor_ident' => $draftInvoice->receptor->numero_identificacion,
                    'receptor_correo'  => $draftInvoice->receptor->correo,
                    'receptor_provincia' => $draftInvoice->receptor->ubicacion['provincia'] ?? null,
                    'receptor_canton' => $draftInvoice->receptor->ubicacion['canton'] ?? null,
                    'receptor_distrito' => $draftInvoice->receptor->ubicacion['distrito'] ?? null,
                    'receptor_barrio' => $draftInvoice->receptor->ubicacion['barrio'] ?? null,
                    'receptor_otras_sennas' => $draftInvoice->receptor->ubicacion['sennas'] ?? null,
                ] : [];
           
                $documentId = DB::table('documentos')->insertGetId([
                    'sucursal_id'       => $branchData['sucursal_id'],
                    'clave'             => $claveNumerica,
                    'consecutivo'       => $consecutivoNumerico,
                    'tipo_documento'    => $tipoDoc,
                    'proveedor_sistemas' => $draftInvoice->proveedor_sistema,
                    'cod_actividad_emisor'      => $draftInvoice->cod_act_economica_emisor,
                    'cod_actividad_receptor'    => $draftInvoice->cod_act_economica_receptor,
                    'cod_sucursal'      => (string) $branchData['sucursal_code'],
                    'cod_terminal'      => (string) $branchData['sucursal_terminal'],
                    'situacion'         => $situacion,
                    'condicion_venta'   => $draftInvoice->condicion_venta,
                                        'condicion_venta_otros' => $draftInvoice->condicion_venta_otros,
                    'plazo_credito'     => $draftInvoice->plazo_credito,
                    'moneda'            => $draftInvoice->moneda,
                    'tipo_cambio'       => (strtoupper((string)$draftInvoice->moneda) === 'CRC')
                        ? 1.0
                        : (float)$draftInvoice->tipo_cambio,
                    'ambiente'          => $sucursal->mh_ambiente,
                    'xml_firmado'       => $signedXml,
                    'medio_pago'        => $draftInvoice->medio_pago[0]['medio_pago_tipo'] ?? null,
                    'medio_pago_otros'  => $draftInvoice->medio_pago[0]['medio_pago_otro'] ?? null,
                    ...$receptor,
                    'total_serv_gravados'   => $draftInvoice->resumen['total_serv_gravados'],
                    'total_serv_exentos'    => $draftInvoice->resumen['total_serv_exentos'],
                    'total_serv_exonerados' => $draftInvoice->resumen['total_serv_exonerados'],
                    'total_serv_no_sujetos' => $draftInvoice->resumen['total_serv_no_sujetos'],
                    'total_merc_gravadas'   => $draftInvoice->resumen['total_merc_gravadas'],
                    'total_merc_exentas'    => $draftInvoice->resumen['total_merc_exentas'],
                    'total_merc_exoneradas' => $draftInvoice->resumen['total_merc_exoneradas'],
                    'total_merc_no_sujetas' => $draftInvoice->resumen['total_merc_no_sujetas'],
                    'total_gravado'         => $draftInvoice->resumen['total_gravado'],
                    'total_exento'          => $draftInvoice->resumen['total_exento'],
                    'total_exonerado'       => $draftInvoice->resumen['total_exonerado'],
                    'total_no_sujeto'       => $draftInvoice->resumen['total_no_sujeto'],
                    'total_venta'           => $draftInvoice->resumen['total_venta'],
                    'total_descuentos'      => $draftInvoice->resumen['total_descuentos'],
                    'total_venta_neta'      => $draftInvoice->resumen['total_venta_neta'],
                    'total_impuesto'        => $draftInvoice->resumen['total_impuesto'],
                    'total_imp_asum_emi'    => $draftInvoice->resumen['total_imp_asum_emi'],
                                        'total_iva_devuelto'    => $draftInvoice->resumen['total_iva_devuelto'],
                    'total_desglose_impuestos_json' => json_encode($draftInvoice->desglose_impuestos, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                    'total_comprobante'     => $draftInvoice->resumen['total_comprobante'],
                    'fecha_emision'         => $now->toAtomString(),
                    'estado'                => 'firmado',
                    'xml_schema_version'    => '4.4',
                    'ref_tipo_documento'    => $draftInvoice->info_referencia['tipo_doc'] ?? null,
                    'ref_clave'             => $draftInvoice->info_referencia['clave'] ?? null,
                    'ref_fecha_emision'     => $draftInvoice->info_referencia['fecha_emision'] ?? null,
                    'ref_codigo'            => $draftInvoice->info_referencia['codigo'] ?? null,
                    'ref_razon'             => $draftInvoice->info_referencia['razon'] ?? null
                ]);

                foreach (($draftInvoice->medio_pago ?? []) as $index => $medioPago) {
                    DB::table('documento_medios_pago')->insert([
                        'documento_id' => $documentId,
                        'tipo_medio_pago' => $medioPago['medio_pago_tipo'] ?? null,
                        'medio_pago_otro' => $medioPago['medio_pago_otro'] ?? null,
                        'total_medio_pago' => $medioPago['medio_pago_total'] ?? null,
                        'orden' => $index + 1,
                    ]);
                }

                foreach ($draftInvoice->lineas as $line) {
                    $detailId = DB::table('documento_detalles')->insertGetId([
                        'num_linea'         => $line->numero_linea,
                        'codigo_cabys'      => $line->codigo_cabys,
                        'unidad_medida'     => $line->unidad_medida,
                        'unidad_medida_comercial' => null,
                        'detalle'           => $line->detalle,
                        'precio_unitario'   => $line->precio_unitario,
                        'cantidad'          => $line->cantidad,
                        'monto_total'       => $line->monto_total,
                        'sub_total'         => $line->sub_total,
                        'base_imponible'    => $line->base_imponible,
                        'codigo_descuento'  => $line->cod_desc ? str_pad((string) $line->cod_desc, 2, '0', STR_PAD_LEFT) : null,
                        'codigo_descuento_otros' => $line->cod_desc_otros,
                        'razon_descuento'   => $line->desc_razon,
                        'monto_descuento'   => $line->desc_total > 0 ? $line->desc_total : null,
                        'codigo_impuesto'   => $line->cod_imp,
                        'codigo_impuesto_otros' => $line->cod_imp_otros,
                        'codigo_tarifa_iva' => $line->cod_imp_iva,
                        'tarifa_impuesto'   => $line->imp_tarifa,
                        'monto_impuesto'    => $line->imp_monto,
                        'impuesto_asumido_emisor' => $line->imp_asum_emisor,
                        'impuesto_iva_fabrica' => $line->imp_asum_emisor,
                        'impuesto_neto'     => $line->impuesto_neto,
                        'monto_total_linea' => $line->monto_total_linea,
                        'documento_id'      => $documentId,
                    ]);

                    foreach (($line->descuentos ?? []) as $desc) {
                        DB::table('documento_detalle_descuentos')->insert([
                            'documento_detalle_id' => $detailId,
                            'codigo_descuento' => $desc->codigo_descuento ?? null,
                            'codigo_descuento_otros' => $desc->codigo_descuento_otros ?? null,
                            'naturaleza_descuento' => $desc->naturaleza_descuento ?? null,
                            'monto_descuento' => $desc->monto_descuento ?? 0,
                        ]);
                    }

                    foreach (($line->impuestos ?? []) as $imp) {
                        DB::table('documento_detalle_impuestos')->insert([
                            'documento_detalle_id' => $detailId,
                            'codigo' => $imp->codigo ?? null,
                            'codigo_tarifa_iva' => $imp->codigo_tarifa_iva ?? null,
                            'tarifa' => $imp->tarifa ?? null,
                            'monto' => $imp->monto ?? 0,
                            'impuesto_asumido_emisor_fabrica' => $line->imp_asum_emisor ?? 0,
                            'impuesto_neto' => $line->impuesto_neto ?? 0,
                        ]);
                    }
                }
                
                return [
                    "doc_id" => $documentId,
                    "fecha" => $now->toAtomString(),
                    "clave" => $claveNumerica,
                    "comprobanteXml" => $signedXml,
                    "emisor" => $data['emisor'],
                    "receptor" => $data['receptor'] ?? null
                ];

            } catch (Throwable $e) {
                throw new Exception("No es posible agregar una factura en este momento. Intenta de nuevo. {$e->getMessage()} {$e->getLine()}");
            }
        });

        try {
            $response = $this->mh_client->sendDocument($result);
        
            if ($response->accepted()) {
                DB::table('documentos')
                    ->where('clave', $result['clave'])
                    ->update([
                        'estado' => 'enviado',
                    ]);
        
                return [
                    'doc_id' => $result['doc_id'],
                    'clave' => $result['clave'],
                    'fecha' => $result['fecha'],
                    'estado' => 'enviado',
                    'enviado' => true,
                ];
            }
        
            $errorCause = null;
            if ($response->hasHeader('X-Error-Cause')) {
                $errorCause = $response->header('X-Error-Cause');
            }
        
            DB::table('documentos')
                ->where('clave', $result['clave'])
                ->update([
                    'estado' => 'error',
                    'respuesta_error_hacienda' => $errorCause ?: $response->body(),
                ]);
        
            return [
                'doc_id' => $result['doc_id'],
                'clave' => $result['clave'],
                'fecha' => $result['fecha'],
                'estado' => 'error',
                'enviado' => false,
                'detalle' => $errorCause ?: 'La emisión no fue aceptada por Hacienda.',
            ];
        } catch (\RuntimeException $e) {
            return [
                'doc_id' => $result['doc_id'],
                'clave' => $result['clave'],
                'fecha' => $result['fecha'],
                'estado' => 'firmado',
                'enviado' => false,
                'detalle' => 'Documento firmado y pendiente de reintento.',
            ];
        }
    }
    
    public function addMensajeReceptor($branchData, $data)
    {
        $data = $this->normalizeMensajeReceptorPayload((array) $data);

        $this->assertMensajeReceptorConsistencia(
            (string) $data['tipo_documento'],
            (string) $data['mensaje']
        );
        
        $exists = DB::table('mensajes_receptor')
            ->where('clave', (string) $data['clave'])
            ->where('mensaje', (string) $data['mensaje'])
            ->whereIn('estado', ['firmado', 'enviado', 'error'])
            ->exists();
        
        if ($exists) {
            throw new Exception('Ya existe un mensaje receptor registrado localmente para esta clave y mensaje.');
        }
    
        $branchEmitter = $data['_branch_emisor'] ?? null;
    
        if (!is_array($branchEmitter) || empty($branchEmitter['numero_identificacion']) || empty($branchEmitter['tipo_identificacion'])) {
            throw new Exception('No fue posible resolver el receptor autenticado desde la sucursal/API key.');
        }
    
        $tipoDocumento = TipoDocumento::from($data['tipo_documento']);
    
        $result = $this->withSiguienteNumero(
            $branchData['sucursal_id'],
            $tipoDocumento->consecutivoCol(),
            function ($sucursal, $consecutivo) use ($branchData, $data, $branchEmitter, $tipoDocumento) {
    
                $now = Carbon::now('America/Costa_Rica');
                $fechaEmisionDoc = !empty($data['fecha_emision_doc'])
                    ? Carbon::parse($data['fecha_emision_doc'])->setTimezone('America/Costa_Rica')->toAtomString()
                    : $now->toAtomString();
    
                $numeroConsecutivoReceptor = ConsecutiveGenerator::generarNumeroConsecutivo(
                    (int) $sucursal->cod_sucursal,
                    (int) $sucursal->cod_terminal,
                    $tipoDocumento->value,
                    $consecutivo
                );
    
                $xmlData = [
                    'clave' => (string) $data['clave'],
                    'numero_cedula_emisor' => (string) $data['numero_cedula_emisor'],
                    'fecha_emision_doc' => $fechaEmisionDoc,
                    'mensaje' => (string) $data['mensaje'],
                    'detalle_mensaje' => $data['detalle_mensaje'] ?? null,
                    'monto_total_impuesto' => isset($data['monto_total_impuesto']) ? (float) $data['monto_total_impuesto'] : null,
                    'codigo_actividad' => $data['codigo_actividad'] ?? null,
                    'condicion_impuesto' => $data['condicion_impuesto'] ?? null,
                    'monto_total_impuesto_acreditar' => isset($data['monto_total_impuesto_acreditar']) ? (float) $data['monto_total_impuesto_acreditar'] : null,
                    'monto_total_de_gasto_aplicable' => isset($data['monto_total_de_gasto_aplicable']) ? (float) $data['monto_total_de_gasto_aplicable'] : null,
                    'total_factura' => (float) $data['total_factura'],
                    'numero_cedula_receptor' => (string) $branchEmitter['numero_identificacion'],
                    'numero_consecutivo_receptor' => $numeroConsecutivoReceptor,
                ];
    
                try {
                    $rawXml = (new MensajeReceptorBuilder())->build($xmlData);
                    $signedXml = $this->signer::sign($branchData, $rawXml);
    
                    $id = DB::table('mensajes_receptor')->insertGetId([
                        'sucursal_id' => $branchData['sucursal_id'],
                        'tipo_documento' => (string) $data['tipo_documento'],
                        'clave' => (string) $data['clave'],
                        'numero_consecutivo_receptor' => $numeroConsecutivoReceptor,
                        'mensaje' => (string) $data['mensaje'],
                        'detalle_mensaje' => $data['detalle_mensaje'] ?? null,
                        'numero_cedula_emisor' => (string) $data['numero_cedula_emisor'],
                        'tipo_identificacion_emisor' => (string) $data['tipo_identificacion_emisor'],
                        'numero_cedula_receptor' => (string) $branchEmitter['numero_identificacion'],
                        'tipo_identificacion_receptor' => (string) $branchEmitter['tipo_identificacion'],
                        'fecha_emision_doc' => $fechaEmisionDoc,
                        'monto_total_impuesto' => isset($data['monto_total_impuesto']) ? (float) $data['monto_total_impuesto'] : null,
                        'codigo_actividad' => $data['codigo_actividad'] ?? null,
                        'condicion_impuesto' => $data['condicion_impuesto'] ?? null,
                        'monto_total_impuesto_acreditar' => isset($data['monto_total_impuesto_acreditar']) ? (float) $data['monto_total_impuesto_acreditar'] : null,
                        'monto_total_de_gasto_aplicable' => isset($data['monto_total_de_gasto_aplicable']) ? (float) $data['monto_total_de_gasto_aplicable'] : null,
                        'total_factura' => (float) $data['total_factura'],
                        'ambiente' => $sucursal->mh_ambiente,
                        'xml_firmado' => $signedXml,
                        'estado' => 'firmado',
                        'created_at' => now(),
                        'updated_at' => now(),
                    ]);
    
                    try {
                        $this->mh_client->useSandboxMode($branchData['isSandbox']);
                        $this->mh_client->authenticate($branchData['username'], $branchData['password']);
    
                        $response = $this->mh_client->sendDocument([
                            'fecha' => $fechaEmisionDoc,
                            'clave' => (string) $data['clave'],
                            'comprobanteXml' => $signedXml,
                            'emisor' => [
                                'tipo_identificacion' => (string) $data['tipo_identificacion_emisor'],
                                'numero_identificacion' => (string) $data['numero_cedula_emisor'],
                            ],
                            'receptor' => [
                                'tipo_identificacion' => (string) $branchEmitter['tipo_identificacion'],
                                'numero_identificacion' => (string) $branchEmitter['numero_identificacion'],
                            ],
                        ]);
    
                        if ($response->accepted()) {
                            DB::table('mensajes_receptor')
                                ->where('id', $id)
                                ->update([
                                    'estado' => 'enviado',
                                    'updated_at' => now(),
                                ]);
                        } else {
                            DB::table('mensajes_receptor')
                                ->where('id', $id)
                                ->update([
                                    'estado' => 'error',
                                    'respuesta_error_hacienda' => $response->header('X-Error-Cause') ?: $response->body(),
                                    'updated_at' => now(),
                                ]);
                        }
    
                        return [
                            'id' => $id,
                            'clave' => (string) $data['clave'],
                            'numero_consecutivo_receptor' => $numeroConsecutivoReceptor,
                            'fecha' => $fechaEmisionDoc,
                            'enviado' => $response->accepted() ?? false,
                        ];
                    } catch (\RuntimeException $e) {
                        return [
                            'id' => $id,
                            'clave' => (string) $data['clave'],
                            'numero_consecutivo_receptor' => $numeroConsecutivoReceptor,
                            'fecha' => $fechaEmisionDoc,
                            'enviado' => false,
                            'detalle' => 'Mensaje receptor firmado y pendiente de reintento.',
                        ];
                    }
                } catch (Throwable $e) {
                    throw new Exception("No es posible agregar el mensaje receptor en este momento. {$e->getMessage()}");
                }
            }
        );
    
        return $result;
    }
    
    private function assertMensajeReceptorConsistencia(string $tipoDocumento, string $mensaje): void
    {
        $map = [
            '05' => '1',
            '06' => '2',
            '07' => '3',
        ];
    
        if (!isset($map[$tipoDocumento])) {
            throw new Exception('Tipo de documento de mensaje receptor no válido.');
        }
    
        if ($map[$tipoDocumento] !== $mensaje) {
            throw new Exception('El tipo_documento no coincide con el mensaje indicado.');
        }
    }
    

    private function normalizeMensajeReceptorPayload(array $data): array
    {
        $data['tipo_documento'] = trim((string) ($data['tipo_documento'] ?? ''));
        $data['clave'] = preg_replace('/\D+/', '', (string) ($data['clave'] ?? ''));
        $data['tipo_identificacion_emisor'] = trim((string) ($data['tipo_identificacion_emisor'] ?? ''));
        $data['numero_cedula_emisor'] = preg_replace('/\D+/', '', (string) ($data['numero_cedula_emisor'] ?? ''));
        $data['mensaje'] = trim((string) ($data['mensaje'] ?? ''));
        $data['detalle_mensaje'] = trim((string) ($data['detalle_mensaje'] ?? ''));
        $data['detalle_mensaje'] = $data['detalle_mensaje'] !== '' ? $data['detalle_mensaje'] : null;
        $data['codigo_actividad'] = trim((string) ($data['codigo_actividad'] ?? ''));
        $data['codigo_actividad'] = $data['codigo_actividad'] !== '' ? $data['codigo_actividad'] : null;
        $data['condicion_impuesto'] = trim((string) ($data['condicion_impuesto'] ?? ''));
        $data['condicion_impuesto'] = $data['condicion_impuesto'] !== '' ? $data['condicion_impuesto'] : null;

        $data['monto_total_impuesto'] = $this->toNullableFloat($data['monto_total_impuesto'] ?? null);
        $data['monto_total_impuesto_acreditar'] = $this->toNullableFloat($data['monto_total_impuesto_acreditar'] ?? null);
        $data['monto_total_de_gasto_aplicable'] = $this->toNullableFloat($data['monto_total_de_gasto_aplicable'] ?? null);
        $data['total_factura'] = (float) ($data['total_factura'] ?? 0);

        if ($data['monto_total_impuesto'] !== null && $data['monto_total_impuesto'] <= 0) {
            $data['monto_total_impuesto'] = null;
        }

        if ($data['monto_total_impuesto_acreditar'] !== null && $data['monto_total_impuesto_acreditar'] <= 0) {
            $data['monto_total_impuesto_acreditar'] = null;
        }

        if ($data['monto_total_de_gasto_aplicable'] !== null && $data['monto_total_de_gasto_aplicable'] <= 0) {
            $data['monto_total_de_gasto_aplicable'] = null;
        }

        if ($data['mensaje'] === '3') {
            $data['codigo_actividad'] = null;
            $data['condicion_impuesto'] = null;
            $data['monto_total_impuesto_acreditar'] = null;
            $data['monto_total_de_gasto_aplicable'] = null;
        } elseif ($data['condicion_impuesto'] === '05') {
            $data['codigo_actividad'] = null;
            $data['monto_total_impuesto_acreditar'] = null;
            $data['monto_total_de_gasto_aplicable'] = null;
        }

        return $data;
    }

    private function toNullableFloat($value): ?float
    {
        if ($value === null || $value === '') {
            return null;
        }

        return is_numeric($value) ? (float) $value : null;
    }

    public function consultarMensajeReceptor($branchData, int $id)
    {
        $msg = DB::table('mensajes_receptor')
            ->where('id', $id)
            ->where('sucursal_id', $branchData['sucursal_id'])
            ->first();
    
        if (!$msg) {
            throw new Exception('Mensaje receptor no encontrado para la sucursal indicada.');
        }
    
        return [
            'id' => $msg->id,
            'tipo_documento' => $msg->tipo_documento,
            'clave' => $msg->clave,
            'numero_consecutivo_receptor' => $msg->numero_consecutivo_receptor,
            'mensaje' => $msg->mensaje,
            'detalle_mensaje' => $msg->detalle_mensaje,
            'fecha_emision_doc' => $msg->fecha_emision_doc,
            'estado' => $msg->estado,
            'respuesta_hacienda' => $msg->respuesta_hacienda,
            'respuesta_error_hacienda' => $msg->respuesta_error_hacienda,
            'created_at' => $msg->created_at,
            'updated_at' => $msg->updated_at,
        ];
    }
    
    private function mapMensajeReceptorRow(object $msg): array
    {
        return [
            'id' => $msg->id,
            'sucursal_id' => $msg->sucursal_id,
            'tipo_documento' => $msg->tipo_documento,
            'clave' => $msg->clave,
            'numero_consecutivo_receptor' => $msg->numero_consecutivo_receptor,
            'mensaje' => $msg->mensaje,
            'detalle_mensaje' => $msg->detalle_mensaje,
            'numero_cedula_emisor' => $msg->numero_cedula_emisor,
            'tipo_identificacion_emisor' => $msg->tipo_identificacion_emisor,
            'numero_cedula_receptor' => $msg->numero_cedula_receptor,
            'tipo_identificacion_receptor' => $msg->tipo_identificacion_receptor,
            'fecha_emision_doc' => $msg->fecha_emision_doc,
            'monto_total_impuesto' => $msg->monto_total_impuesto,
            'codigo_actividad' => $msg->codigo_actividad,
            'condicion_impuesto' => $msg->condicion_impuesto,
            'monto_total_impuesto_acreditar' => $msg->monto_total_impuesto_acreditar,
            'monto_total_de_gasto_aplicable' => $msg->monto_total_de_gasto_aplicable,
            'total_factura' => $msg->total_factura,
            'ambiente' => $msg->ambiente,
            'respuesta_hacienda' => $msg->respuesta_hacienda,
            'respuesta_error_hacienda' => $msg->respuesta_error_hacienda,
            'estado' => $msg->estado,
            'created_at' => $msg->created_at,
            'updated_at' => $msg->updated_at,
        ];
    }
    
    public function consultarMensajeReceptorPorClave($branchData, string $clave): array
    {
        $clave = trim($clave);
    
        $items = DB::table('mensajes_receptor')
            ->where('sucursal_id', $branchData['sucursal_id'])
            ->where('clave', $clave)
            ->orderByDesc('id')
            ->get();
    
        if ($items->isEmpty()) {
            throw new Exception('No se encontraron mensajes receptor para la clave indicada en esta sucursal.');
        }
    
        return [
            'clave' => $clave,
            'total' => $items->count(),
            'items' => $items->map(fn($r) => $this->mapMensajeReceptorRow($r))->values()->all(),
        ];
    }
    
    public function listarMensajesReceptor($branchData, array $filters = []): array
    {
        $page  = max(1, (int)($filters['page'] ?? 1));
        $limit = (int)($filters['limit'] ?? 20);
        $limit = max(1, min(100, $limit));
    
        $q = DB::table('mensajes_receptor')
            ->where('sucursal_id', $branchData['sucursal_id']);
    
        $clave = trim((string)($filters['clave'] ?? ''));
        if ($clave !== '') {
            $q->where('clave', $clave);
        }
    
        $estado = trim((string)($filters['estado'] ?? ''));
        if ($estado !== '') {
            $q->where('estado', $estado);
        }
    
        $tipoDocumento = trim((string)($filters['tipo_documento'] ?? ''));
        if ($tipoDocumento !== '') {
            $q->where('tipo_documento', $tipoDocumento);
        }
    
        $mensaje = trim((string)($filters['mensaje'] ?? ''));
        if ($mensaje !== '') {
            $q->where('mensaje', $mensaje);
        }
    
        $desde = trim((string)($filters['desde'] ?? ''));
        if ($desde !== '') {
            $q->whereDate('created_at', '>=', $desde);
        }
    
        $hasta = trim((string)($filters['hasta'] ?? ''));
        if ($hasta !== '') {
            $q->whereDate('created_at', '<=', $hasta);
        }
    
        $total = (clone $q)->count();
    
        $items = $q->orderByDesc('id')
            ->forPage($page, $limit)
            ->get();
    
        return [
            'page' => $page,
            'limit' => $limit,
            'total' => $total,
            'filters' => [
                'clave' => $clave,
                'estado' => $estado,
                'tipo_documento' => $tipoDocumento,
                'mensaje' => $mensaje,
                'desde' => $desde,
                'hasta' => $hasta,
            ],
            'items' => $items->map(fn($r) => $this->mapMensajeReceptorRow($r))->values()->all(),
        ];
    }
    
    private function procesarMensajesReceptorFirmados(): void
    {
        $mensajes = DB::table('mensajes_receptor')
            ->where('estado', 'firmado')
            ->get();
    
        foreach ($mensajes as $msg) {
            $sucursal = DB::table('sucursales')
                ->where('id', $msg->sucursal_id)
                ->first();
    
            if (!$sucursal) {
                continue;
            }
    
            $credenciales = DB::table('mh_credenciales')
                ->where('client_id', $sucursal->cliente_id)
                ->where('mh_environment', $sucursal->mh_ambiente)
                ->first();
    
            if (!$credenciales) {
                continue;
            }
    
            try {
                $this->mh_client->useSandboxMode($credenciales->mh_environment === 'sandbox');
                $this->mh_client->authenticate($credenciales->mh_username, $credenciales->mh_password);
    
                $response = $this->mh_client->sendDocument([
                    'fecha' => $msg->fecha_emision_doc,
                    'clave' => $msg->clave,
                    'comprobanteXml' => $msg->xml_firmado,
                    'emisor' => [
                        'tipo_identificacion' => $msg->tipo_identificacion_emisor,
                        'numero_identificacion' => $msg->numero_cedula_emisor,
                    ],
                    'receptor' => [
                        'tipo_identificacion' => $msg->tipo_identificacion_receptor,
                        'numero_identificacion' => $msg->numero_cedula_receptor,
                    ],
                ]);
    
                if ($response->accepted()) {
                    DB::table('mensajes_receptor')
                        ->where('id', $msg->id)
                        ->update([
                            'estado' => 'enviado',
                            'updated_at' => now(),
                        ]);
                } else {
                    DB::table('mensajes_receptor')
                        ->where('id', $msg->id)
                        ->update([
                            'estado' => 'error',
                            'respuesta_error_hacienda' => $response->header('X-Error-Cause') ?: $response->body(),
                            'updated_at' => now(),
                        ]);
                }
            } catch (Throwable $e) {
                continue;
            }
        }
    }

    protected function formatDecimal(float $amount, int $decimals = 5): string
    {
        return number_format($amount,$decimals,".","");
    }

    public function getTipoCaByS($codigo) 
    {
        $primerDigito = (int) substr($codigo, 0, 1);

        if ($primerDigito >= 0 && $primerDigito <= 4) {
            return 'Unid';
        } elseif ($primerDigito >= 5 && $primerDigito <= 9) {
            return 'Sp';
        }
    }

    public function enrich(array $data): ElectronicDocument
    {
        $enrichedLines = [];

        $totalServGravados      = 0;
        $totalServExentos       = 0;
        $totalServExonerados    = 0;
        $totalServNoSujetos     = 0;
        $totalMercGravadas      = 0;
        $totalMercExentas       = 0;
        $totalMercExoneradas    = 0;
        $totalMercNoSujetas     = 0;

        $totalImpuestos         = 0;
        $totalDescuentos        = 0;
        $totalesImpuestoPorCodigo = [];
       
        $impuestoAsumEmisor     = 0;

        foreach ($data['lineas'] as $index => $linea) 
        {
            $montoTotal = (float) $this->formatDecimal(((float) $linea['cantidad']) * ((float) $linea['precio_unitario']), 5);
            $codigoCaByS = $linea['codigo_cabys'];
            $tipoCaByS = $this->getTipoCaByS($codigoCaByS);

            $descuentosArray = [];
            $descuentoTotal = 0.0;
            $codigoDescuento = null;
            $codigoDescuentoOtros = null;
            $descuentoNaturaleza = null;

            foreach (($linea['descuentos'] ?? []) as $discountIndex => $descuento) {
                if (!is_array($descuento) || $descuento === []) {
                    continue;
                }
                $montoDescuento = (float) ($descuento['monto_descuento'] ?? 0);
                $descuentoTotal += $montoDescuento;
                $descuentosArray[] = DiscountData::from([
                    'codigo_descuento' => $descuento['codigo_descuento'] ?? null,
                    'codigo_descuento_otros' => $descuento['codigo_descuento_otros'] ?? null,
                    'monto_descuento' => $montoDescuento,
                    'naturaleza_descuento' => $descuento['naturaleza_descuento'] ?? null,
                ]);
                if ($discountIndex === 0) {
                    $codigoDescuento = $descuento['codigo_descuento'] ?? null;
                    $codigoDescuentoOtros = $descuento['codigo_descuento_otros'] ?? null;
                    $descuentoNaturaleza = $descuento['naturaleza_descuento'] ?? null;
                }
            }

            $descuentoTotal = (float) $this->formatDecimal($descuentoTotal);
            $totalDescuentos += $descuentoTotal;
            $subTotal = (float) $this->formatDecimal($montoTotal - $descuentoTotal);

            $indicadorBoniRegalia = ($linea['ind_boniregalia'] ?? false);
            $base = $indicadorBoniRegalia ? $montoTotal : $subTotal;

            $impuestosArray = [];
            $impuestoMontoTotal = 0.0;
            $codigoImpuesto = null;
            $codigoImpuestoOtros = null;
            $codigoTarifaIva = null;
            $impuestoTarifa = 0.0;
            $ivaCodigoTarifa = null;
            $ivaCodigo = null;

            foreach (($linea['impuestos'] ?? []) as $taxIndex => $impuesto) {
                if (!is_array($impuesto) || $impuesto === []) {
                    continue;
                }

                $codigo = $impuesto['codigo'] ?? null;
                $codigoTarifa = $impuesto['codigo_tarifa_iva'] ?? null;
                $tarifa = (float) ($impuesto['tarifa'] ?? 0);
                $monto = (float) $this->formatDecimal($base * ($tarifa / 100));
                $impuestoMontoTotal += $monto;

                $impuestosArray[] = TaxData::from([
                    'codigo' => $codigo,
                    'codigo_tarifa_iva' => $codigoTarifa,
                    'tarifa' => $tarifa,
                    'monto' => $monto,
                ]);

                if ($taxIndex === 0) {
                    $codigoImpuesto = $codigo;
                    $codigoImpuestoOtros = $impuesto['codigo_impuesto_otros'] ?? null;
                    $codigoTarifaIva = $codigoTarifa;
                    $impuestoTarifa = $tarifa;
                }

                if (in_array($codigo, [CodigoImpuesto::IMPUESTO_AL_VALOR_AGREGADO->value, CodigoImpuesto::IVA_CALCULO_ESPECIAL->value], true) && $ivaCodigoTarifa === null) {
                    $ivaCodigo = $codigo;
                    $ivaCodigoTarifa = $codigoTarifa;
                }

                $desgloseImpuestoKey = (string) $codigo . '_' . (string) $codigoTarifa;
                if (isset($totalesImpuestoPorCodigo[$desgloseImpuestoKey])) {
                    $totalesImpuestoPorCodigo[$desgloseImpuestoKey]['total_monto_impuesto'] += !$indicadorBoniRegalia ? $monto : 0;
                } else {
                    $totalesImpuestoPorCodigo[$desgloseImpuestoKey] = [
                        'codigo' => $codigo,
                        'codigo_tarifa_iva' => $codigoTarifa,
                        'total_monto_impuesto' => !$indicadorBoniRegalia ? $monto : 0,
                    ];
                }
            }

            $esNoSujeto = $ivaCodigo !== null && in_array($ivaCodigoTarifa, [CodigoTarifaIVA::TARIFA_CERO->value, CodigoTarifaIVA::TARIFA_CERO_SIN_DERECHO_CREDITO->value], true);
            $esExento = !$esNoSujeto && $ivaCodigo !== null && $ivaCodigoTarifa === CodigoTarifaIVA::TARIFA_EXENTA->value;

            if ($tipoCaByS === 'Sp') {
                if ($esNoSujeto) {
                    $totalServNoSujetos += $montoTotal;
                } elseif ($esExento) {
                    $totalServExentos += $montoTotal;
                } else {
                    $totalServGravados += $montoTotal;
                }
            } else {
                if ($esNoSujeto) {
                    $totalMercNoSujetas += $montoTotal;
                } elseif ($esExento) {
                    $totalMercExentas += $montoTotal;
                } else {
                    $totalMercGravadas += $montoTotal;
                }
            }

            $impuestoMontoTotal = (float) $this->formatDecimal($impuestoMontoTotal);
            $impuestoAsumEmisorUno = 0.0;
            if ($indicadorBoniRegalia) {
                $impuestoAsumEmisorUno = $impuestoMontoTotal;
                $impuestoAsumEmisor += $impuestoMontoTotal;
            } else {
                $totalImpuestos += $impuestoMontoTotal;
            }

            $impuestoNeto = (float) $this->formatDecimal($impuestoMontoTotal - $impuestoAsumEmisorUno);

            $enrichedLines[] = LineaItemData::from([
                'numero_linea' => $index + 1,
                'codigo_cabys' => $codigoCaByS,
                'cantidad' => (float) $this->formatDecimal($linea['cantidad'], 3),
                'unidad_medida' => $linea['unidad_medida'] ?? $tipoCaByS,
                'detalle' => $linea['detalle'],
                'precio_unitario' => (float) $this->formatDecimal($linea['precio_unitario']),
                'monto_total' => $montoTotal,
                'cod_desc' => $codigoDescuento,
                'cod_desc_otros' => $codigoDescuentoOtros,
                'desc_razon' => $descuentoNaturaleza,
                'desc_total' => $descuentoTotal,
                'sub_total' => $subTotal,
                'base_imponible' => $subTotal,
                'cod_imp' => $codigoImpuesto ?? CodigoImpuesto::IMPUESTO_AL_VALOR_AGREGADO->value,
                'cod_imp_otros' => $codigoImpuestoOtros,
                'cod_imp_iva' => (int) ($codigoTarifaIva ?? 0),
                'imp_tarifa' => (float) $this->formatDecimal($impuestoTarifa, 2),
                'imp_monto' => $impuestoMontoTotal,
                'imp_asum_emisor' => (float) $this->formatDecimal($impuestoAsumEmisorUno),
                'impuesto_neto' => $impuestoNeto,
                'monto_total_linea' => (float) $this->formatDecimal($subTotal + $impuestoNeto),
                'impuestos' => $impuestosArray,
                'descuentos' => $descuentosArray,
            ]);
        }

        $totalGravado       = $totalServGravados   + $totalMercGravadas;
        $totalExento        = $totalServExentos    + $totalMercExentas;
        $totalExonerado     = $totalServExonerados + $totalMercExoneradas;
        $totalNoSujeto      = $totalServNoSujetos  + $totalMercNoSujetas;
        $totalVenta         = $totalGravado + $totalExento + $totalExonerado + $totalNoSujeto;
        $totalVentaNeta     = $totalVenta - $totalDescuentos;
        $totalIVADevuelto   = 0;
        $totalImpAsumEmi    = $impuestoAsumEmisor;
        $totalComprobante   = $totalVentaNeta + $totalImpuestos;

        $resumen = [
            'total_serv_gravados'   => $totalServGravados,
            'total_serv_exentos'    => $totalServExentos,
            'total_serv_exonerados' => $totalServExonerados,
            'total_serv_no_sujetos' => $totalServNoSujetos,
            'total_merc_gravadas'   => $totalMercGravadas,
            'total_merc_exentas'    => $totalMercExentas,
            'total_merc_exoneradas' => $totalMercExoneradas,
            'total_merc_no_sujetas' => $totalMercNoSujetas,
            'total_gravado'         => $totalGravado,
            'total_exento'          => $totalExento,
            'total_exonerado'       => $totalExonerado,
            'total_no_sujeto'       => $totalNoSujeto,
            'total_venta'           => $totalVenta,
            'total_descuentos'      => $totalDescuentos,
            'total_venta_neta'      => $totalVentaNeta,
            'total_impuesto'        => $totalImpuestos,
            'total_imp_asum_emi'    => $totalImpAsumEmi,
            'total_iva_devuelto'    => $totalIVADevuelto,
            'total_comprobante'     => $totalComprobante,
        ];
        
        $tipoDocumentoActual = (string) ($data['tipo_documento'] ?? '');
        $tipoReferencia = (string) ($data['info_referencia']['tipo_doc'] ?? '');
        $codigoReferencia = (string) ($data['info_referencia']['codigo'] ?? '');

        if (in_array($tipoDocumentoActual, ['02', '03'], true)) {
            $codigosReferenciaPermitidos = ['01', '02', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '99'];
            if ($codigoReferencia === '03' || !in_array($codigoReferencia, $codigosReferenciaPermitidos, true)) {
                throw new \Exception('Código de referencia inválido para NC/ND. El código 03 no existe como motivo de referencia; use 02 para corregir monto.');
            }
        }

        if (!empty($data['medio_pago']) && is_array($data['medio_pago'])) {
            $runningMediosPago = 0.0;
            $lastMedioPagoIndex = array_key_last($data['medio_pago']);
            foreach ($data['medio_pago'] as $indexMedioPago => &$medioPagoItem) {
                if ($indexMedioPago === $lastMedioPagoIndex) {
                    $medioPagoItem['medio_pago_total'] = (float) $this->formatDecimal($totalComprobante - $runningMediosPago);
                } else {
                    $medioPagoItem['medio_pago_total'] = (float) $this->formatDecimal((float) ($medioPagoItem['medio_pago_total'] ?? 0));
                    $runningMediosPago += (float) $medioPagoItem['medio_pago_total'];
                }
            }
            unset($medioPagoItem);
        }

        $receptorIdent = preg_replace('/\D+/', '', (string) ($data['receptor']['numero_identificacion'] ?? '')) ?: '';
        if ($tipoDocumentoActual === '04' && ($receptorIdent === '' || $receptorIdent === '000000000' || preg_match('/^0+$/', $receptorIdent) === 1)) {
            $data['receptor'] = null;
        }

        if (in_array($tipoDocumentoActual, ['02', '03'], true) && $tipoReferencia === '04') {
            $refClave = (string) ($data['info_referencia']['clave'] ?? '');
            $ref = $refClave !== '' ? DB::table('documentos')->where('clave', $refClave)->first() : null;
            if ($ref === null || (empty($ref->receptor_tipo_ident) && empty($ref->receptor_ident))) {
                $data['receptor'] = null;
            }
        }

        $receptorIdent = preg_replace('/\D+/', '', (string) ($data['receptor']['numero_identificacion'] ?? '')) ?: '';
        if ($receptorIdent === '000000000' || preg_match('/^0+$/', $receptorIdent) === 1) {
            $data['receptor'] = null;
        }

        if (collect($data['receptor']['ubicacion'] ?? [])->filter()->isEmpty()) {
            $data['receptor']['ubicacion'] = null;
        }

        if (collect($data['receptor'] ?? [])->filter()->isEmpty()) {
            $data['receptor'] = null;
        }

        return ElectronicDocument::from([
            "cod_act_economica_emisor" => $data["cod_act_economica_emisor"],
            "cod_act_economica_receptor" => $data["cod_act_economica_receptor"] ?? null,
            "proveedor_sistema"     => config('services.mh.proveedor_cedula') ?? $data["emisor"]["numero_identificacion"],
            "nombre_comercial"      => $data["branch"]["nombre_comercial"] ?? null,
            "condicion_venta"       => $data['condicion_venta'],
            "condicion_venta_otros" => $data['condicion_venta_otros'] ?? null,
            "medio_pago"            => $data['medio_pago'],
            "plazo_credito"         => $data['plazo_credito'] ?? 0,
            "moneda"                => $data["moneda"],
            "tipo_cambio" => (function () use ($data) {
                $moneda = strtoupper(trim((string)($data['moneda'] ?? 'CRC')));
                $tipoDoc = (string)($data['tipo_documento'] ?? '');
            
                // ND=02, NC=03: SIEMPRE copiar moneda y TC del documento referenciado
                if (\in_array($tipoDoc, ['02','03'], true)) {
                    $refClave = (string)($data['info_referencia']['clave'] ?? '');
                    if ($refClave === '') {
                        throw new \Exception("NC/ND requiere info_referencia.clave.");
                    }
            
                    $ref = DB::table('documentos')->where('clave', $refClave)->first();
                    if (!$ref) {
                        throw new \Exception("Documento de referencia no encontrado: {$refClave}");
                    }
            
                    $monedaRef = strtoupper((string)($ref->moneda ?? 'CRC'));
                    $tcRef = (float)($ref->tipo_cambio ?? 1);
            
                    // Forzar consistencia: la moneda de NC/ND debe ser la misma del documento referenciado
                    if ($moneda !== $monedaRef) {
                        throw new \Exception("Moneda de NC/ND debe coincidir con la del documento referenciado ({$monedaRef}).");
                    }
            
                    return ($monedaRef === 'CRC') ? 1.0 : $tcRef;
                }
            
                // CRC: siempre 1
                if ($moneda === 'CRC') return 1.0;
            
                // Si viene en el payload, úsa (y valida > 0)
                $tc = $data['tipo_cambio'] ?? null;
                if ($tc !== null && (float)$tc > 0) return (float)$tc;
            
                // Si no vino, lo obtenemos del servicio (por moneda)
                $resp  = $this->tc_client->obtenerCambioActual(['bccr'], $moneda);
                $venta = $resp['venta'] ?? null;
            
                if ($venta === null || (float)$venta <= 0) {
                    throw new \Exception("Tipo de cambio requerido para moneda {$moneda} y no se pudo obtener automáticamente.");
                }
            
                return (float)$venta;
            })(),
            "emisor"                => $data['emisor'],
            "receptor"              => $data['receptor'] ?? null,
            "lineas"                => $enrichedLines,
            "resumen"               => $resumen,
            "desglose_impuestos"    => $totalesImpuestoPorCodigo,
            "info_referencia"       => $data['info_referencia'] ?? null
        ]);
    }

    private function withSiguienteNumero($sucursalId, $tipoDocumento, Closure $callback) 
    {
        // Hvd8KTAepfEN1qtK
        return DB::transaction(function () use ($sucursalId, $tipoDocumento, $callback) 
        {
            $sucursal = DB::table('sucursales')
                ->where('id', $sucursalId)
                ->lockForUpdate()
                ->first();

            if (!$sucursal) {
                throw new Exception("No se ha encontrado una sucursal para obtener el consecutivo");
            }

            // SE PUEDE HACER UN INDICADOR DE SUCURSAL ACTIVA O NO
            try {
                $nextNumber = $sucursal->$tipoDocumento + 1;
            } catch (Throwable $e) {
                throw new Exception('No se ha encontrado una numeracion consecutiva para este tipo de documento');
            }

            $result = $callback($sucursal, $nextNumber);

            DB::table('sucursales')
                ->where('id', $sucursalId)
                ->update([$tipoDocumento => $nextNumber]);

            return $result;
        }, 2);
    }
}