<?php

namespace App\Services;

use App\Data\DiscountData;
use App\Data\ElectronicDocument;
use App\Data\ExoneracionData;
use App\Data\LineaItemData;
use App\Data\TaxData;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
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']);
        if ($tipoDocumento === TipoDocumento::FEC) {
            $data = $this->prepareFecData($branchData, (array) $data);
        }
        $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['_transmision_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($this->filterExistingColumns('documentos', [
                    '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,
                    'fec_caso' => $data['caso_fec'] ?? null,
                    'proveedor_compra_nombre' => $data['_fec_proveedor_compra']['nombre'] ?? null,
                    'proveedor_compra_tipo_ident' => $data['_fec_proveedor_compra']['tipo_identificacion'] ?? null,
                    'proveedor_compra_ident' => $data['_fec_proveedor_compra']['numero_identificacion'] ?? null,
                    'proveedor_compra_correo' => $data['_fec_proveedor_compra']['correo'] ?? null,
                    'proveedor_compra_otras_sennas_extranjero' => $data['_fec_proveedor_compra']['otras_sennas_extranjero'] ?? null,
                    'proveedor_compra_json' => isset($data['_fec_proveedor_compra']) ? json_encode($data['_fec_proveedor_compra'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
                    'fec_payload_json' => (($data['tipo_documento'] ?? null) === TipoDocumento::FEC->value) ? json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
                    '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'] ?? $draftInvoice->info_referencia['numero'] ?? null,
                    'ref_numero_documento'  => $draftInvoice->info_referencia['numero'] ?? $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_exonerado' => $line->imp_exonerado,
                        '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) {
                        $exoneracionData = $imp->exoneracion ? $imp->exoneracion->toArray() : null;
                        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,
                            'monto_exoneracion' => $imp->monto_exoneracion ?? 0,
                            'impuesto_asumido_emisor_fabrica' => $line->imp_asum_emisor ?? 0,
                            'impuesto_neto' => $imp->impuesto_neto ?? 0,
                            'exoneracion_tipo_documento_ex' => $exoneracionData['tipo_documento_ex'] ?? null,
                            'exoneracion_numero_documento' => $exoneracionData['numero_documento'] ?? null,
                            'exoneracion_tipo_documento_ex_otros' => $exoneracionData['tipo_documento_ex_otros'] ?? null,
                            'exoneracion_articulo' => $exoneracionData['articulo'] ?? null,
                            'exoneracion_inciso' => $exoneracionData['inciso'] ?? null,
                            'exoneracion_nombre_institucion' => $exoneracionData['nombre_institucion'] ?? null,
                            'exoneracion_nombre_institucion_otros' => $exoneracionData['nombre_institucion_otros'] ?? null,
                            'exoneracion_fecha_emision' => $exoneracionData['fecha_emision'] ?? null,
                            'exoneracion_tarifa' => $exoneracionData['tarifa_exonerada'] ?? 0,
                            'exoneracion_json' => $exoneracionData ? json_encode($exoneracionData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
                        ]);
                    }
                }
                
                return [
                    "doc_id" => $documentId,
                    "fecha" => $now->toAtomString(),
                    "clave" => $claveNumerica,
                    "comprobanteXml" => $signedXml,
                    "emisor" => $data['_transmision_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.',
            ];
        }
    }
    
    private function prepareFecData(array $branchData, array $data): array
    {
        $comprador = $data['emisor'] ?? [];
        $proveedor = $this->normalizeFecProveedor((array) ($data['proveedor_compra'] ?? []));
        $receptor = $this->normalizeFecComprador((array) $comprador);

        $actividadBaseComprador = $this->normalizeActivityCode(
            $branchData['actividad_economica']
                ?? $comprador['cod_actividad']
                ?? ''
        );

        $actividadPayload = $this->normalizeActivityCode(
            $data['cod_act_economica_receptor']
                ?? ''
        );

        // En FEC el receptor es el comprador que firma/transmite.
        // Debe usarse la actividad real de la sucursal/credencial fiscal autenticada.
        // No se debe tomar primero un valor normalizado desde la pantalla, porque puede convertir 6201.0 en 620100.
        $actividadReceptor = $actividadBaseComprador ?? $actividadPayload;

        if ($actividadReceptor === null) {
            throw new Exception('La FEC requiere código de actividad económica del comprador/receptor.');
        }

        $actividadEmisor = $this->normalizeActivityCode((string) ($data['proveedor_compra']['cod_actividad'] ?? ''));
        $referencia = (array) ($data['info_referencia'] ?? []);
        $referenciaNumero = trim((string) ($referencia['numero'] ?? $referencia['clave'] ?? ''));
        if ($referenciaNumero === '') {
            throw new Exception('La FEC requiere número o consecutivo del documento de respaldo.');
        }

        $referencia['numero'] = $referenciaNumero;
        if (($referencia['tipo_doc'] ?? '') === '16' && ($proveedor['tipo_identificacion'] ?? '') !== '05') {
            throw new Exception('La referencia 16 solo corresponde a proveedor extranjero no domiciliado en FEC.');
        }

        if (($proveedor['tipo_identificacion'] ?? '') === '06' && (string) ($data['condicion_venta'] ?? '') !== '13') {
            throw new Exception('Para proveedor no contribuyente en FEC se debe usar condición de venta 13.');
        }

        $data['_transmision_emisor'] = $comprador;
        $data['_fec_proveedor_compra'] = $proveedor;
        $data['emisor'] = $proveedor;
        $data['receptor'] = $receptor;
        $data['cod_act_economica_emisor'] = $actividadEmisor;
        $data['cod_act_economica_receptor'] = $actividadReceptor;
        $data['info_referencia'] = $referencia;

        return $data;
    }

    private function normalizeFecProveedor(array $proveedor): array
    {
        $tipo = trim((string) ($proveedor['tipo_identificacion'] ?? ''));
        $ubicacion = $proveedor['ubicacion'] ?? null;
        if (is_array($ubicacion) && collect($ubicacion)->filter()->isEmpty()) {
            $ubicacion = null;
        }

        $result = [
            'nombre' => trim((string) ($proveedor['nombre'] ?? '')),
            'nombre_comercial' => $proveedor['nombre_comercial'] ?? null,
            'tipo_identificacion' => $tipo,
            'numero_identificacion' => trim((string) ($proveedor['numero_identificacion'] ?? '')),
            'correo' => trim((string) ($proveedor['correo'] ?? '')),
            'ubicacion' => $ubicacion,
            'otras_sennas_extranjero' => trim((string) ($proveedor['otras_sennas_extranjero'] ?? '')),
        ];

        if (in_array($tipo, ['05', '06'], true)) {
            $result['ubicacion'] = null;
        }

        if ($tipo === '05' && $result['otras_sennas_extranjero'] === '') {
            throw new Exception('El proveedor extranjero no domiciliado requiere otras señas extranjero.');
        }

        if ($tipo !== '05') {
            $result['otras_sennas_extranjero'] = null;
        }

        return $result;
    }

    private function normalizeFecComprador(array $comprador): array
    {
        $ubicacion = $comprador['ubicacion'] ?? null;
        if (is_array($ubicacion) && collect($ubicacion)->filter()->isEmpty()) {
            $ubicacion = null;
        }

        return [
            'nombre' => trim((string) ($comprador['nombre'] ?? '')),
            'nombre_comercial' => $comprador['nombre_comercial'] ?? null,
            'tipo_identificacion' => trim((string) ($comprador['tipo_identificacion'] ?? '')),
            'numero_identificacion' => trim((string) ($comprador['numero_identificacion'] ?? '')),
            'ubicacion' => $ubicacion,
            'telefono' => $comprador['telefono'] ?? null,
            'correo' => trim((string) ($comprador['correo'] ?? '')),
        ];
    }

    private function normalizeActivityCode(?string $value): ?string
    {
        $value = trim((string) $value);
        if ($value === '') {
            return null;
        }

        $value = str_replace(',', '.', $value);

        // Hacienda V4.4 valida contra el RUT y acepta el código como string de 6 caracteres.
        // En algunos contribuyentes el código registrado viene con punto, por ejemplo 6201.0.
        // No se debe eliminar el punto ni convertirlo automáticamente a 620100.
        if (preg_match('/^\d{4}\.\d$/', $value) === 1) {
            return $value;
        }

        if (preg_match('/^\d{6}$/', $value) === 1) {
            return $value;
        }

        if (preg_match('/^\d{5}$/', $value) === 1) {
            return substr($value, 0, 4) . '.' . substr($value, 4, 1);
        }

        return null;
    }

    private function resolveProveedorSistema(array $data): string
    {
        $configured = trim((string) (config('services.mh.proveedor_cedula') ?? ''));
        if ($configured !== '') {
            return $configured;
        }

        // En FEC, data.emisor es el proveedor de la compra.
        // La identificación correcta para ProveedorSistemas no debe salir del proveedor extranjero.
        $transmitter = trim((string) ($data['_transmision_emisor']['numero_identificacion'] ?? ''));
        if ($transmitter !== '') {
            return $transmitter;
        }

        return trim((string) ($data['emisor']['numero_identificacion'] ?? ''));
    }

    private function filterExistingColumns(string $table, array $data): array
    {
        try {
            $columns = array_flip(Schema::getColumnListing($table));
            return array_intersect_key($data, $columns);
        } catch (Throwable) {
            return $data;
        }
    }

    public function addMensajeReceptor($branchData, $data)
    {
        $data = $this->normalizeMensajeReceptorPayload((array) $data);

        $this->assertMensajeReceptorConsistencia(
            (string) $data['tipo_documento'],
            (string) $data['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.');
        }

        $requestedId = (int) ($data['core_mensaje_receptor_id'] ?? $data['mensaje_receptor_id'] ?? 0);
        $existingTarget = null;
        if ($requestedId > 0) {
            $existingTarget = DB::table('mensajes_receptor')
                ->where('id', $requestedId)
                ->where('sucursal_id', $branchData['sucursal_id'])
                ->first();
        }

        $duplicateQuery = DB::table('mensajes_receptor')
            ->where('clave', (string) $data['clave'])
            ->where('mensaje', (string) $data['mensaje'])
            ->whereIn('estado', ['firmado', 'enviado', 'procesando', 'aceptado', 'rechazado', 'error']);

        if ($existingTarget) {
            $duplicateQuery->where('id', '<>', (int) $existingTarget->id);
        }

        $duplicate = $duplicateQuery->orderByDesc('id')->first();
        if (!$existingTarget && $duplicate) {
            $row = $this->mapMensajeReceptorRow($duplicate);
            $row['duplicado'] = true;
            $row['enviado'] = in_array((string) $duplicate->estado, ['enviado', 'aceptado', 'rechazado'], true);
            $row['detalle'] = (string) ($duplicate->respuesta_error_hacienda ?: 'El mensaje receptor ya estaba registrado para esta clave.');
            return $row;
        }

        $tipoDocumento = TipoDocumento::from($data['tipo_documento']);
        $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();

        $buildAndSend = function (int $id, string $numeroConsecutivoReceptor) use ($branchData, $data, $branchEmitter, $fechaEmisionDoc) {
            $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,
            ];

            $rawXml = (new MensajeReceptorBuilder())->build($xmlData);
            $signedXml = $this->signer::sign($branchData, $rawXml);

            DB::table('mensajes_receptor')
                ->where('id', $id)
                ->update($this->filterExistingColumns('mensajes_receptor', [
                    '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' => $branchData['isSandbox'] ? 'sandbox' : 'production',
                    'origen' => 'contador',
                    'validado_previo' => 1,
                    'observacion_validacion' => 'Mensaje receptor generado desde compras recibidas del panel contable.',
                    'webhook_url' => $data['webhook_url'] ?? null,
                    'webhook_token' => $data['webhook_token'] ?? null,
                    'xml_firmado' => $signedXml,
                    'respuesta_hacienda' => null,
                    'respuesta_error_hacienda' => null,
                    'estado' => 'firmado',
                    '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'],
                    ],
                ]);

                $sentToMh = method_exists($response, 'successful') ? $response->successful() : $response->accepted();

                if ($sentToMh) {
                    $responseXml = '';
                    $xmlState = '';
                    $xmlDetail = '';

                    try {
                        $reception = $this->mh_client->getDocumentReceptionStatus((string) $data['clave']);
                        $responseXml = (string) ($reception->response_xml ?? '');
                        $xmlState = $this->extractMhStateFromXmlPayload($responseXml);
                        $xmlDetail = $this->extractMhDetailFromXmlPayload($responseXml);
                    } catch (Throwable) {
                        $responseXml = '';
                        $xmlState = '';
                        $xmlDetail = '';
                    }

                    $update = [
                        'estado' => $xmlState !== '' ? $this->mapMensajeReceptorEstadoFinal($xmlState) : 'procesando',
                        'respuesta_hacienda' => $xmlState !== '' ? $this->normalizeMensajeReceptorMhState($xmlState) : null,
                        'respuesta_error_hacienda' => $xmlState !== '' && $xmlDetail !== '' ? $xmlDetail : null,
                        'updated_at' => now(),
                    ];

                    if ($responseXml !== '') {
                        $update['xml_respuesta'] = $responseXml;
                    }

                    DB::table('mensajes_receptor')
                        ->where('id', $id)
                        ->update($this->filterExistingColumns('mensajes_receptor', $update));
                } else {
                    $errorCause = $this->haciendaReceptionErrorDetail($response);
                    DB::table('mensajes_receptor')
                        ->where('id', $id)
                        ->update([
                            'estado' => 'firmado',
                            'respuesta_hacienda' => null,
                            'respuesta_error_hacienda' => $errorCause !== ''
                                ? 'XML firmado. No se obtuvo XML de respuesta de Hacienda en este intento: ' . $errorCause
                                : 'XML firmado. No se obtuvo XML de respuesta de Hacienda en este intento.',
                            'updated_at' => now(),
                        ]);
                }

                $fresh = DB::table('mensajes_receptor')->where('id', $id)->first();
                $row = $fresh ? $this->mapMensajeReceptorRow($fresh) : [];
                $row['enviado'] = (bool) $sentToMh;
                $row['sent'] = $row['enviado'];
                $row['fecha'] = $fechaEmisionDoc;
                $row['http_status'] = (int) $response->status();
                if ($row['enviado'] && empty($row['xml_respuesta'])) {
                    $row['detalle'] = 'Mensaje receptor enviado a Hacienda. Pendiente de XML de respuesta.';
                } elseif (!$row['enviado'] && empty($row['detalle'])) {
                    $row['detalle'] = (string) ($row['respuesta_error_hacienda'] ?? 'XML firmado. Pendiente de reintento de envío a Hacienda.');
                }
                return $row;
            } catch (\RuntimeException $e) {
                DB::table('mensajes_receptor')
                    ->where('id', $id)
                    ->update([
                        'estado' => 'firmado',
                        'respuesta_error_hacienda' => 'Mensaje receptor firmado y pendiente de reintento.',
                        'updated_at' => now(),
                    ]);

                $fresh = DB::table('mensajes_receptor')->where('id', $id)->first();
                $row = $fresh ? $this->mapMensajeReceptorRow($fresh) : [];
                $row['enviado'] = false;
                $row['sent'] = false;
                $row['fecha'] = $fechaEmisionDoc;
                $row['detalle'] = 'Mensaje receptor firmado y pendiente de reintento.';
                return $row;
            }
        };

        try {
            if ($existingTarget) {
                $numeroConsecutivoReceptor = trim((string) ($existingTarget->numero_consecutivo_receptor ?? ''));
                if ($numeroConsecutivoReceptor === '') {
                    throw new Exception('El mensaje receptor existente no tiene consecutivo asignado. Genere nuevamente el trámite.');
                }

                return $buildAndSend((int) $existingTarget->id, $numeroConsecutivoReceptor);
            }

            return $this->withSiguienteNumero(
                $branchData['sucursal_id'],
                $tipoDocumento->consecutivoCol(),
                function ($sucursal, $consecutivo) use ($branchData, $data, $branchEmitter, $tipoDocumento, $fechaEmisionDoc, $buildAndSend) {
                    $numeroConsecutivoReceptor = ConsecutiveGenerator::generarNumeroConsecutivo(
                        (int) $sucursal->cod_sucursal,
                        (int) $sucursal->cod_terminal,
                        $tipoDocumento->value,
                        $consecutivo
                    );

                    $id = DB::table('mensajes_receptor')->insertGetId($this->filterExistingColumns('mensajes_receptor', [
                        '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,
                        'origen' => 'contador',
                        'validado_previo' => 1,
                        'observacion_validacion' => 'Mensaje receptor generado desde compras recibidas del panel contable.',
                        'webhook_url' => $data['webhook_url'] ?? null,
                        'webhook_token' => $data['webhook_token'] ?? null,
                        'estado' => 'pendiente_envio',
                        'created_at' => now(),
                        'updated_at' => now(),
                    ]));

                    return $buildAndSend($id, $numeroConsecutivoReceptor);
                }
            );
        } catch (Throwable $e) {
            throw new Exception("No es posible agregar el mensaje receptor en este momento. {$e->getMessage()}");
        }
    }

    private function normalizeMensajeReceptorMhState(string $state): string
    {
        $state = strtolower(trim($state));
        $state = strtr($state, [
            'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u',
            'Á' => 'a', 'É' => 'e', 'Í' => 'i', 'Ó' => 'o', 'Ú' => 'u',
        ]);

        if (str_contains($state, 'acept')) {
            return 'aceptado';
        }
        if (str_contains($state, 'rechaz')) {
            return 'rechazado';
        }
        if (str_contains($state, 'error') || str_contains($state, 'fall')) {
            return 'error';
        }
        if (str_contains($state, 'proces') || str_contains($state, 'recib') || str_contains($state, 'pend') || str_contains($state, 'envi')) {
            return 'procesando';
        }

        return mb_substr($state, 0, 60);
    }

    private function mapMensajeReceptorEstadoFinal(string $state): string
    {
        $state = $this->normalizeMhReceptionState($state);

        return match ($state) {
            'aceptado' => 'aceptado',
            'rechazado' => 'rechazado',
            'error' => 'error',
            default => 'procesando',
        };
    }

    private function normalizeMhReceptionState(string $state): string
    {
        $state = strtolower(trim($state));
        $state = strtr($state, [
            'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u',
            'Á' => 'a', 'É' => 'e', 'Í' => 'i', 'Ó' => 'o', 'Ú' => 'u',
        ]);

        if ($state === '') {
            return '';
        }

        if (str_contains($state, 'acept')) {
            return 'aceptado';
        }

        if (str_contains($state, 'rechaz')) {
            return 'rechazado';
        }

        if (str_contains($state, 'error') || str_contains($state, 'fall')) {
            return 'error';
        }

        if (str_contains($state, 'proces') || str_contains($state, 'recib') || str_contains($state, 'pend') || str_contains($state, 'envi')) {
            return 'procesando';
        }

        return mb_substr($state, 0, 50);
    }

    private function extractMhStateFromXmlPayload(string $xml): string
    {
        return $this->normalizeMhReceptionState($this->extractXmlNodeText($xml, ['EstadoMensaje', 'IndEstado', 'Estado', 'RespuestaHacienda']));
    }

    private function extractMhDetailFromXmlPayload(string $xml): string
    {
        return $this->extractXmlNodeText($xml, ['DetalleMensaje', 'Detalle', 'Mensaje']);
    }

    private function extractXmlNodeText(string $xml, array $names): string
    {
        $xml = $this->normalizeXmlPayload($xml);
        if ($xml === '' || !str_starts_with(ltrim($xml), '<')) {
            return '';
        }

        $previous = libxml_use_internal_errors(true);
        $node = simplexml_load_string($xml);
        libxml_clear_errors();
        libxml_use_internal_errors($previous);

        if ($node === false) {
            return '';
        }

        foreach ($names as $name) {
            $found = $node->xpath('//*[local-name()="' . $name . '"]');
            if (is_array($found)) {
                foreach ($found as $value) {
                    $text = trim((string) $value);
                    if ($text !== '') {
                        return $text;
                    }
                }
            }
        }

        return '';
    }

    private function normalizeXmlPayload(string $body): string
    {
        $body = trim($body);
        if ($body === '' || str_starts_with(ltrim($body), '<')) {
            return $body;
        }

        $decoded = json_decode($body, true);
        if (is_string($decoded)) {
            return $this->normalizeXmlPayload($decoded);
        }
        if (is_array($decoded)) {
            foreach (['respuesta-xml', 'respuesta_xml', 'respuestaXml', 'xml_respuesta', 'xmlRespuesta', 'xml_response', 'response_xml'] as $key) {
                if (isset($decoded[$key]) && !is_array($decoded[$key]) && trim((string) $decoded[$key]) !== '') {
                    return $this->normalizeXmlPayload((string) $decoded[$key]);
                }
            }
        }

        $clean = preg_replace('/\s+/', '', $body) ?? $body;
        if ($clean !== '' && preg_match('/^[A-Za-z0-9+\/]+=*$/', $clean) === 1) {
            $decoded64 = base64_decode($clean, true);
            if (is_string($decoded64) && trim($decoded64) !== '') {
                return $this->normalizeXmlPayload($decoded64);
            }
        }

        return $body;
    }

    private function haciendaReceptionErrorDetail($response): string
    {
        $detail = '';
        try {
            if (method_exists($response, 'hasHeader') && $response->hasHeader('X-Error-Cause')) {
                $detail = trim((string) $response->header('X-Error-Cause'));
            }
        } catch (Throwable) {
            $detail = '';
        }

        if ($detail === '') {
            try {
                $json = $response->json();
                if (is_array($json)) {
                    foreach (['respuesta_error_hacienda', 'detalle', 'message', 'mensaje', 'error'] as $key) {
                        if (isset($json[$key]) && !is_array($json[$key]) && trim((string) $json[$key]) !== '') {
                            $detail = trim((string) $json[$key]);
                            break;
                        }
                    }
                    if ($detail === '' && $json !== []) {
                        $encoded = json_encode($json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                        $detail = is_string($encoded) ? $encoded : '';
                    }
                }
            } catch (Throwable) {
                $detail = '';
            }
        }

        if ($detail === '') {
            try {
                $detail = trim((string) $response->body());
            } catch (Throwable) {
                $detail = '';
            }
        }

        return mb_substr($detail, 0, 1000);
    }

    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('/[^0-9A-Za-z]/', '', (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;
        }

        $data['mensaje_receptor_id'] = (int) ($data['mensaje_receptor_id'] ?? 0);
        $data['core_mensaje_receptor_id'] = (int) ($data['core_mensaje_receptor_id'] ?? 0);
        $data['webhook_url'] = trim((string) ($data['webhook_url'] ?? $data['callback_url'] ?? '')) ?: null;
        $data['webhook_token'] = trim((string) ($data['webhook_token'] ?? $data['callback_token'] ?? '')) ?: 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.');
        }

        $msg = $this->refreshMensajeReceptorDesdeHacienda($branchData, $msg);
    
        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,
            'xml_firmado' => $msg->xml_firmado ?? null,
            'xml_respuesta' => $msg->xml_respuesta ?? null,
            'xml_response' => $msg->xml_respuesta ?? null,
            'ultimo_webhook_payload_json' => $msg->ultimo_webhook_payload_json ?? null,
            'created_at' => $msg->created_at,
            'updated_at' => $msg->updated_at,
        ];
    }
    
    private function refreshMensajeReceptorDesdeHacienda(array $branchData, object $msg): object
    {
        $currentXml = trim((string) ($msg->xml_respuesta ?? ''));
        $currentState = $this->extractMhStateFromXmlPayload($currentXml);
        if ($currentXml !== '' && $currentState !== '') {
            return $msg;
        }

        try {
            $this->mh_client->useSandboxMode((bool) ($branchData['isSandbox'] ?? false));
            $this->mh_client->authenticate((string) ($branchData['username'] ?? ''), (string) ($branchData['password'] ?? ''));
            $reception = $this->mh_client->getDocumentReceptionStatus((string) ($msg->clave ?? ''));
            $xmlRespuesta = (string) ($reception->response_xml ?? '');
            $xmlState = $this->extractMhStateFromXmlPayload($xmlRespuesta);
            $xmlDetail = $this->extractMhDetailFromXmlPayload($xmlRespuesta);

            $update = [
                'estado' => $xmlState !== '' ? $this->mapMensajeReceptorEstadoFinal($xmlState) : 'procesando',
                'respuesta_hacienda' => $xmlState !== '' ? $this->normalizeMensajeReceptorMhState($xmlState) : null,
                'respuesta_error_hacienda' => $xmlState !== '' ? ($xmlDetail !== '' ? $xmlDetail : null) : null,
                'updated_at' => now(),
            ];

            if (trim($xmlRespuesta) !== '') {
                $update['xml_respuesta'] = $xmlRespuesta;
            }

            DB::table('mensajes_receptor')
                ->where('id', $msg->id)
                ->update($this->filterExistingColumns('mensajes_receptor', $update));

            return DB::table('mensajes_receptor')->where('id', $msg->id)->first() ?: $msg;
        } catch (Throwable) {
            return $msg;
        }
    }

    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,
            'xml_firmado' => $msg->xml_firmado ?? null,
            'xml_respuesta' => $msg->xml_respuesta ?? null,
            'xml_response' => $msg->xml_respuesta ?? null,
            'ultimo_webhook_payload_json' => $msg->ultimo_webhook_payload_json ?? null,
            '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.');
        }

        $items = $items->map(fn($r) => $this->refreshMensajeReceptorDesdeHacienda($branchData, $r));
    
        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,
                    ],
                ]);
    
                $sentToMh = method_exists($response, 'successful') ? $response->successful() : $response->accepted();

                if ($sentToMh) {
                    DB::table('mensajes_receptor')
                        ->where('id', $msg->id)
                        ->update([
                            'estado' => 'procesando',
                            'respuesta_hacienda' => null,
                            'respuesta_error_hacienda' => null,
                            'updated_at' => now(),
                        ]);
                } else {
                    DB::table('mensajes_receptor')
                        ->where('id', $msg->id)
                        ->update([
                            'estado' => 'firmado',
                            'respuesta_hacienda' => null,
                            'respuesta_error_hacienda' => 'XML firmado. No se obtuvo XML de respuesta de Hacienda en este intento: ' . ($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;
            $impuestoExoneradoTotal = 0.0;
            $impuestoNetoTotal = 0.0;
            $tieneExoneracion = false;
            $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));
                $exoneracion = $this->normalizarExoneracionLinea($impuesto['exoneracion'] ?? null, $codigoCaByS, $monto, $tarifa);
                $montoExoneracion = $exoneracion ? (float) $exoneracion['monto_exoneracion'] : 0.0;
                $impuestoNetoItem = (float) $this->formatDecimal(max(0, $monto - $montoExoneracion));

                $impuestoMontoTotal += $monto;
                $impuestoExoneradoTotal += $montoExoneracion;
                $impuestoNetoTotal += $impuestoNetoItem;
                $tieneExoneracion = $tieneExoneracion || $montoExoneracion > 0;

                $impuestosArray[] = TaxData::from([
                    'codigo' => $codigo,
                    'codigo_tarifa_iva' => $codigoTarifa,
                    'tarifa' => $tarifa,
                    'monto' => $monto,
                    'monto_exoneracion' => $montoExoneracion,
                    'impuesto_neto' => $impuestoNetoItem,
                    'exoneracion' => $exoneracion ? ExoneracionData::from($exoneracion) : null,
                ]);

                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 ? $impuestoNetoItem : 0;
                } else {
                    $totalesImpuestoPorCodigo[$desgloseImpuestoKey] = [
                        'codigo' => $codigo,
                        'codigo_tarifa_iva' => $codigoTarifa,
                        'total_monto_impuesto' => !$indicadorBoniRegalia ? $impuestoNetoItem : 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;
                } elseif ($tieneExoneracion) {
                    $totalServExonerados += $montoTotal;
                } else {
                    $totalServGravados += $montoTotal;
                }
            } else {
                if ($esNoSujeto) {
                    $totalMercNoSujetas += $montoTotal;
                } elseif ($esExento) {
                    $totalMercExentas += $montoTotal;
                } elseif ($tieneExoneracion) {
                    $totalMercExoneradas += $montoTotal;
                } else {
                    $totalMercGravadas += $montoTotal;
                }
            }

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

            $impuestoNeto = (float) $this->formatDecimal($impuestoNetoTotal - $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_exonerado' => $impuestoExoneradoTotal,
                '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"     => $this->resolveProveedorSistema($data),
            "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 normalizarExoneracionLinea($exoneracion, string $codigoCaByS, float $montoImpuesto, float $tarifaImpuesto): ?array
    {
        if (!is_array($exoneracion) || $exoneracion === []) {
            return null;
        }

        if (array_key_exists('vigente', $exoneracion) && (bool) $exoneracion['vigente'] === false) {
            throw new Exception('La autorización de exoneración no está vigente.');
        }

        $fechaVencimiento = trim((string) ($exoneracion['fecha_vencimiento'] ?? ''));
        if ($fechaVencimiento !== '' && Carbon::parse($fechaVencimiento)->lt(Carbon::today('America/Costa_Rica'))) {
            throw new Exception('La autorización de exoneración está vencida.');
        }

        $cabysPermitidos = $exoneracion['cabys'] ?? [];
        if (is_string($cabysPermitidos)) {
            $decoded = json_decode($cabysPermitidos, true);
            $cabysPermitidos = is_array($decoded) ? $decoded : [];
        }

        if (is_array($cabysPermitidos) && $cabysPermitidos !== [] && !in_array($codigoCaByS, array_map('strval', $cabysPermitidos), true)) {
            throw new Exception("El CABYS {$codigoCaByS} no está incluido en la autorización de exoneración.");
        }

        $tipoDocumentoEx = trim((string) ($exoneracion['tipo_documento_ex'] ?? $exoneracion['tipo_documento'] ?? ''));
        $numeroDocumento = trim((string) ($exoneracion['numero_documento'] ?? $exoneracion['autorizacion'] ?? ''));
        $fechaEmision = trim((string) ($exoneracion['fecha_emision'] ?? ''));
        $tarifaExonerada = (float) ($exoneracion['tarifa_exonerada'] ?? $exoneracion['porcentaje_exoneracion'] ?? 0);

        if ($tipoDocumentoEx === '' || $numeroDocumento === '' || $fechaEmision === '' || $tarifaExonerada <= 0) {
            throw new Exception('La exoneración requiere tipo de documento, número, fecha de emisión y tarifa exonerada.');
        }

        $montoExoneracion = isset($exoneracion['monto_exoneracion']) && $exoneracion['monto_exoneracion'] !== ''
            ? (float) $exoneracion['monto_exoneracion']
            : (float) $this->formatDecimal($montoImpuesto * min($tarifaExonerada, max($tarifaImpuesto, 0)) / max($tarifaImpuesto, 0.00001));

        $montoExoneracion = (float) $this->formatDecimal(min($montoImpuesto, max(0, $montoExoneracion)));

        return [
            'tipo_documento_ex' => str_pad($tipoDocumentoEx, 2, '0', STR_PAD_LEFT),
            'numero_documento' => $numeroDocumento,
            'tipo_documento_ex_otros' => $exoneracion['tipo_documento_ex_otros'] ?? null,
            'articulo' => $exoneracion['articulo'] ?? null,
            'inciso' => $exoneracion['inciso'] ?? null,
            'nombre_institucion' => trim((string) ($exoneracion['codigo_institucion'] ?? $exoneracion['nombre_institucion'] ?? '')) ?: null,
            'nombre_institucion_otros' => $exoneracion['nombre_institucion_otros'] ?? null,
            'fecha_emision' => Carbon::parse($fechaEmision)->toAtomString(),
            'tarifa_exonerada' => (float) $this->formatDecimal($tarifaExonerada, 2),
            'monto_exoneracion' => $montoExoneracion,
        ];
    }

    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);
    }
}