<?php
require_once 'models/PseModel.php';
class PseController
{

    // public function enviarFactura(int $id_venta): array
    // {
    //     $apiUrl = 'https://facturalahoy.com/api/facturalaya/factura';
    //     //$apiUrl = 'https://facturalahoy.com/api/factura';
    //     $dataModel = new PseModel();
    //     $data = $dataModel->getDataComprobante($id_venta);

    //     var_dump($data);

    //     // Empresa (RUC y modo)
    //     $empresa = $dataModel->getDataEmpresa();
    //     if (!$empresa || empty($empresa['ruc'])) {
    //         return ['ok' => false, 'error' => 'No se encontró la empresa o falta RUC'];
    //     }
    //     $ambiente = ((int) $empresa['modo'] === 1) ? 'produccion' : 'beta';

    //     // === RUTAS DE DESTINO ===
    //     // Base de filesystem: subimos desde /facturacion/controllers a /facturacion
    //     $baseFacturacion = realpath(dirname(__DIR__)); // .../facturacion
    //     if ($baseFacturacion === false) {
    //         // Fallback por si realpath falla (p.ej. permisos o symlinks)
    //         $baseFacturacion = dirname(__DIR__);
    //     }

    //     // Carpeta donde se guardarán los ZIP (filesystem)
    //     $dir_entorno = rtrim($baseFacturacion, '/\\')
    //         . "/UBL21/archivos_xml_sunat/cpe_xml/{$ambiente}/{$empresa['ruc']}/";

    //     // URL pública opcional para servir esos archivos (si tienes constante URL)
    //     // Ej: https://tusitio.com/pale/restaurantepro/facturacion/UBL21/archivos_xml_sunat/cpe_xml/{ambiente}/{ruc}/
    //     $url_publica_entorno = rtrim(URL, '/')
    //         . "/facturacion/UBL21/archivos_xml_sunat/cpe_xml/{$ambiente}/{$empresa['ruc']}/";

    //     try {
    //         // Llamar a la API
    //         $resp = $this->enviarFacturaApi($apiUrl, $data);


    //         var_dump($resp);

    //         // 1) Validar HTTP
    //         $http = (int) ($resp['status'] ?? 0);
    //         if ($http !== 200) {
    //             throw new \RuntimeException('HTTP inesperado: ' . $http);
    //         }

    //         // 2) Validar JSON
    //         $json = $resp['json'] ?? null;
    //         if (!$json) {
    //             throw new \RuntimeException('JSON inválido: ' . ($resp['body'] ?? ''));
    //         }
    //         if (($json['respuesta'] ?? '') !== 'ok') {
    //             $detalle = $json['msj_sunat'] ?? $json['mensaje'] ?? 'sin mensaje';
    //             throw new \RuntimeException('La API no respondió OK: ' . $detalle);
    //         }

    //         $mensaje = $json['resp_sunat']['mensaje'] ?? ($json['msj_sunat'] ?? null);
    //         $hashCdr = $json['resp_sunat']['hash_cdr'] ?? ($json['hash_cdr'] ?? null);
    //         $hashCpe = $json['hash_cpe'] ?? null;

    //         $files = [
    //             'xml_cpe' => $json['name_file_xml_cpe'] ?? null,
    //             'zip_cpe' => $json['name_file_zip_cpe'] ?? null,
    //             'xml_cdr' => $json['name_file_xml_cdr'] ?? null,
    //             'zip_cdr' => $json['name_file_zip_cdr'] ?? null,
    //         ];

    //         // 3) Rutas devueltas por el API (por si las quieres mostrar)
    //         $rutasApi = [
    //             'ruta_xml' => $json['ruta_xml'] ?? null,
    //             'ruta_cdr' => $json['ruta_cdr'] ?? null,
    //             'ruta_pdf' => $json['ruta_pdf'] ?? null,
    //             'ruta_cpe_zip' => $json['ruta_cpe_zip'] ?? null,
    //             'ruta_cdr_zip' => $json['ruta_cdr_zip'] ?? null,
    //         ];

    //         // 4) Asegurar carpeta local
    //         if (!is_dir($dir_entorno) && !mkdir($dir_entorno, 0775, true) && !is_dir($dir_entorno)) {
    //             throw new \RuntimeException('No se pudo crear la carpeta: ' . $dir_entorno);
    //         }

    //         // 5) Guardar ZIPs en Base64 (si vienen)
    //         $guardarZipB64 = function (?string $b64, string $nombre) use ($dir_entorno): ?string {
    //             if (!$b64)
    //                 return null;
    //             $bin = base64_decode($b64, true);
    //             if ($bin === false) {
    //                 throw new \RuntimeException("Base64 inválido para $nombre");
    //             }
    //             $path = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombre;
    //             if (file_put_contents($path, $bin) === false) {
    //                 throw new \RuntimeException("No se pudo guardar $nombre en $path");
    //             }
    //             return $path;
    //         };

    //         $nombreZipCpe = $json['name_file_zip_cpe'] ?? 'cpe.zip';
    //         $nombreZipCdr = $json['name_file_zip_cdr'] ?? 'cdr.zip';

    //         $zipCpeLocal = $guardarZipB64($json['file_cpe_zip'] ?? null, $nombreZipCpe);
    //         $zipCdrLocal = $guardarZipB64($json['file_cdr_zip'] ?? null, $nombreZipCdr);

    //         // URLs públicas correspondientes a lo guardado (opcional)
    //         $zipCpeUrl = $zipCpeLocal ? ($url_publica_entorno . $nombreZipCpe) : null;
    //         $zipCdrUrl = $zipCdrLocal ? ($url_publica_entorno . $nombreZipCdr) : null;

    //         // 6) Bloque SUNAT
    //         $respSunat = $json['resp_sunat'] ?? null;

    //         $respUpdate = $dataModel->setDataDocumento($id_venta, 1, $http, $mensaje, $nombreZipCpe, $hashCdr, $hashCpe, "a");

    //         if (!$respUpdate) {
    //             throw new RuntimeException('No se pudo actualizar tm_venta (execute() falló).');
    //         }

    //         // 7) Retornar todo lo útil
    //         return [
    //             'ok' => true,
    //             'http' => $http,
    //             'resp_sunat' => $respSunat,
    //             'rutas_api' => $rutasApi,
    //             'dir_entorno' => $dir_entorno,      // dónde se guardó
    //             'zip_cpe_local' => $zipCpeLocal,      // path local completo
    //             'zip_cdr_local' => $zipCdrLocal,      // path local completo
    //             'zip_cpe_url' => $zipCpeUrl,        // URL pública (si aplica)
    //             'zip_cdr_url' => $zipCdrUrl,        // URL pública (si aplica)
    //             'nombres_zip' => [
    //                 'cpe' => $nombreZipCpe,
    //                 'cdr' => $nombreZipCdr,
    //             ],
    //         ];



    //     } catch (\Throwable $e) {
    //         return ['ok' => false, 'error' => $e->getMessage()];
    //     }
    // }

    // private function enviarFacturaApi(string $url, array $payload, array $extraHeaders = [], int $timeout = 60): array
    // {

    //     $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    //     if ($payloadJson === false) {
    //         throw new RuntimeException('No se pudo codificar el payload a JSON: ' . json_last_error_msg());
    //     }

    //     $ch = curl_init($url);

    //     $headers = array_merge([
    //         'Content-Type: application/json',
    //         'Accept: application/json',
    //     ], $extraHeaders);

    //     $options = [
    //         CURLOPT_RETURNTRANSFER => true,
    //         CURLOPT_POST => true,
    //         CURLOPT_HTTPHEADER => $headers,
    //         CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
    //         CURLOPT_CONNECTTIMEOUT => 20,   // segundos para conectar
    //         CURLOPT_TIMEOUT => $timeout, // segundos totales
    //         CURLOPT_FOLLOWLOCATION => true,
    //         CURLOPT_MAXREDIRS => 5,
    //         CURLOPT_SSL_VERIFYPEER => true, // mantener true en producción
    //         CURLOPT_SSL_VERIFYHOST => 2,
    //     ];

    //     curl_setopt_array($ch, $options);

    //     $body = curl_exec($ch);
    //     $errNo = curl_errno($ch);
    //     $errMsg = curl_error($ch);
    //     $info = curl_getinfo($ch);
    //     curl_close($ch);

    //     if ($errNo) {

    //         throw new RuntimeException("Error cURL #$errNo: $errMsg");
    //     }

    //     $status = (int) ($info['http_code'] ?? 0);

    //     $decoded = null;
    //     if (is_string($body) && $body !== '') {
    //         $decoded = json_decode($body, true);
    //     }

    //     // Opcional: tratar códigos 4xx/5xx como error
    //     if ($status >= 400) {
    //         // Puedes registrar $body para depurar
    //         throw new RuntimeException("La API respondió HTTP $status. Respuesta: " . substr($body ?? '', 0, 1000));
    //     }

    //     return [
    //         'status' => $status,
    //         'headers' => $info,
    //         'body' => $body,
    //         'json' => $decoded,
    //         'payloadJson' => $payloadJson,
    //     ];
    // }



    private function sanitizePayload(array $payload): array
    {
        $data = $payload;


        // RUC cliente como STRING limpio
        if (isset($data['cliente_numerodocumento'])) {
            $doc = preg_replace('/\D+/', '', (string) $data['cliente_numerodocumento']);
            $data['cliente_numerodocumento'] = $doc;
        }

        // S.A.C => S.A.C.
        if (!empty($data['cliente_nombre']) && is_string($data['cliente_nombre'])) {
            if (preg_match('/\bS\.A\.C$/u', $data['cliente_nombre'])) {
                $data['cliente_nombre'] .= '.';
            }
        }

        // Totales numéricos
        $keysNumericas = [
            'total_gravadas',
            'total_exoneradas',
            'total_inafecta',
            'total_gratuitas',
            'total_exportacion',
            'total_isc',
            'total_icbper',
            'total_otr_imp',
            'total_descuento',
            'impuesto_icbper',
            'porcentaje_igv',
            'total_igv',
            'sub_total',
            'total',
            'monto_deuda_total'
        ];
        foreach ($keysNumericas as $k) {
            if (isset($data[$k]))
                $data[$k] = (float) $data[$k];
        }

        // Detalle: ITEM_DET entero y campos numéricos a float
        if (isset($data['detalle']) && is_array($data['detalle'])) {
            foreach ($data['detalle'] as $i => $dl) {
                $data['detalle'][$i]['ITEM_DET'] = (int) ($dl['ITEM_DET'] ?? ($i + 1));
                foreach (['CANTIDAD_DET', 'PRECIO_DET', 'PRECIO_SIN_IGV_DET', 'IGV_DET', 'ICBPER_DET', 'ISC_DET', 'IMPORTE_DET', 'PORCENTAJE_DESCUENTO', 'MONTO_DESCUENTO'] as $nk) {
                    if (isset($dl[$nk]))
                        $data['detalle'][$i][$nk] = (float) $dl[$nk];
                }
                $data['detalle'][$i]['UNIDAD_MEDIDA_DET'] = $dl['UNIDAD_MEDIDA_DET'] ?? 'NIU';
                $data['detalle'][$i]['PRECIO_TIPO_CODIGO'] = $dl['PRECIO_TIPO_CODIGO'] ?? '01';
                $data['detalle'][$i]['COD_TIPO_OPERACION_DET'] = $dl['COD_TIPO_OPERACION_DET'] ?? '10';
                $data['detalle'][$i]['DESCUENTO_ITEM'] = $dl['DESCUENTO_ITEM'] ?? 'no';
                $data['detalle'][$i]['CODIGO_DESCUENTO'] = $dl['CODIGO_DESCUENTO'] ?? '00';
            }
        }

        // Coherencia exonerada
        if (($data['tipo_operacion'] ?? '') === '0200') {
            $data['porcentaje_igv'] = (float) ($data['porcentaje_igv'] ?? 0);
            $data['total_igv'] = (float) ($data['total_igv'] ?? 0);
        }

        return $data;
    }

    private function enviarFacturaApi(string $url, array $payload, array $extraHeaders = [], int $timeout = 60, bool $verbose = false): array
    {
        // Sanea y serializa una sola vez (SIN JSON_NUMERIC_CHECK)
        $data = $this->sanitizePayload($payload);
        $requestJson = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($requestJson === false) {
            throw new \RuntimeException('No se pudo codificar el payload a JSON: ' . json_last_error_msg());
        }

        // Verificación: RUC va como string en el JSON final
        if (isset($data['cliente_numerodocumento'])) {
            $needle = '"cliente_numerodocumento":"' . $data['cliente_numerodocumento'] . '"';
            if (strpos($requestJson, $needle) === false) {
                throw new \RuntimeException('El RUC del cliente no está serializado como string en el JSON final.');
            }
        }

        $ch = curl_init($url);

        $headers = array_merge([
            'Content-Type: application/json',
            'Accept: application/json',
            'Content-Length: ' . strlen($requestJson),
        ], $extraHeaders);

        // Capturar cabeceras de respuesta
        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POSTFIELDS => $requestJson,
            CURLOPT_HEADER => true,   // <-- para capturar headers + body
            CURLOPT_CONNECTTIMEOUT => 20,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ];
        if ($verbose)
            $options[CURLOPT_VERBOSE] = true;

        curl_setopt_array($ch, $options);

        $raw = curl_exec($ch);
        $errNo = curl_errno($ch);
        $errMsg = curl_error($ch);
        $info = curl_getinfo($ch);
        curl_close($ch);

        if ($errNo) {
            throw new \RuntimeException("Error cURL #$errNo: $errMsg");
        }

        $status = (int) ($info['http_code'] ?? 0);

        // Separar headers y body
        $headerSize = (int) ($info['header_size'] ?? 0);
        $rawHeaders = substr($raw, 0, $headerSize);
        $body = substr($raw, $headerSize);

        // Normalizar headers en array simple (opcional)
        $respHeaders = [];
        foreach (preg_split("/\r\n/", $rawHeaders) as $line) {
            if (strpos($line, ':') !== false) {
                [$k, $v] = explode(':', $line, 2);
                $respHeaders[trim($k)] = trim($v);
            }
        }

        $json = null;
        if (is_string($body) && $body !== '') {
            $tmp = json_decode($body, true);
            if (json_last_error() === JSON_ERROR_NONE)
                $json = $tmp;
        }

        // NUNCA lanzamos excepción aquí: devolvemos todo para que puedas ver el JSON de respuesta
        return [
            'status' => $status,
            'headers' => $respHeaders,
            'body' => $body,
            'json' => $json,
            'request_json' => $requestJson, // <- lo que realmente enviaste
            'info' => $info,        // meta de cURL (incluye URL final)
        ];
    }

    public function enviarFactura(int $id_venta): array
    {
        //$apiUrl = 'https://facturalahoy.com/api/facturalaya/factura';
        $apiUrl = 'https://facturalahoy.com/api/factura';

        $dataModel = new PseModel();
        $data = $dataModel->getDataComprobante($id_venta);
        if (!$data) {
            return ['ok' => false, 'error' => 'No se encontró la venta'];
        }

        // Empresa (para determinar ambiente/rutas locales)
        $empresa = $dataModel->getDataEmpresa();
        if (!$empresa || empty($empresa['ruc'])) {
            return ['ok' => false, 'error' => 'No se encontró la empresa o falta RUC'];
        }
        $ambiente = ((int) $empresa['modo'] === 1) ? 'produccion' : 'beta';

        // === RUTEO DE ARCHIVOS ===
        $baseFacturacion = realpath(dirname(__DIR__)) ?: dirname(__DIR__);
        $dir_entorno = rtrim($baseFacturacion, '/\\')
            . "/UBL21/archivos_xml_sunat/cpe_xml/{$ambiente}/{$empresa['ruc']}/";

        $url_publica_entorno = defined('URL')
            ? rtrim(URL, '/') . "/facturacion/UBL21/archivos_xml_sunat/cpe_xml/{$ambiente}/{$empresa['ruc']}/"
            : null;

        try {
            // === 1) LLAMADA A LA API (no lanzar excepción en 4xx/5xx para poder leer body) ===
            $resp = $this->enviarFacturaApi($apiUrl, $data, [], 60, false);

            var_dump($data);
            var_dump($resp);

            $http = (int) ($resp['status'] ?? 0);
            $json = $resp['json'] ?? null;
            $okApi = ($http === 200) && is_array($json) && (($json['respuesta'] ?? '') === 'ok');

            // === 2) ASEGURAR CARPETA DESTINO ===
            if (!is_dir($dir_entorno) && !mkdir($dir_entorno, 0775, true) && !is_dir($dir_entorno)) {
                throw new \RuntimeException('No se pudo crear la carpeta: ' . $dir_entorno);
            }

            // === 3) EXTRAER CAMPOS ÚTILES DEL JSON ===
            $mensaje = $json['resp_sunat']['mensaje'] ?? ($json['msj_sunat'] ?? ($json['mensaje'] ?? null));
            $hashCdr = $json['resp_sunat']['hash_cdr'] ?? ($json['hash_cdr'] ?? null);
            $hashCpe = $json['hash_cpe'] ?? null;

            $nombreZipCpe = $json['name_file_zip_cpe'] ?? null;
            $nombreZipCdr = $json['name_file_zip_cdr'] ?? null;

            $zipCpeLocal = null;
            $zipCdrLocal = null;

            // === 4) GUARDAR ZIPs EN BASE64 (si vienen) ===
            $guardarZipB64 = function (?string $b64, ?string $nombre) use ($dir_entorno): ?string {
                if (!$b64 || !$nombre)
                    return null;
                $bin = base64_decode($b64, true);
                if ($bin === false) {
                    throw new \RuntimeException("Base64 inválido para $nombre");
                }
                $path = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombre;
                if (file_put_contents($path, $bin) === false) {
                    throw new \RuntimeException("No se pudo guardar $nombre en $path");
                }
                return $path;
            };

            if ($okApi) {
                $zipCpeLocal = $guardarZipB64($json['file_cpe_zip'] ?? null, $nombreZipCpe);
                $zipCdrLocal = $guardarZipB64($json['file_cdr_zip'] ?? null, $nombreZipCdr);

                // === 4.1) Fallback: si no viene base64, intenta descargar desde rutas ===
                if (!$zipCpeLocal && !empty($json['ruta_cpe_zip'])) {
                    $nombreZipCpe = $nombreZipCpe ?: basename(parse_url($json['ruta_cpe_zip'], PHP_URL_PATH));
                    $tmpPath = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombreZipCpe;
                    @file_put_contents($tmpPath, @file_get_contents($json['ruta_cpe_zip']));
                    if (is_file($tmpPath))
                        $zipCpeLocal = $tmpPath;
                }
                if (!$zipCdrLocal && !empty($json['ruta_cdr_zip'])) {
                    $nombreZipCdr = $nombreZipCdr ?: basename(parse_url($json['ruta_cdr_zip'], PHP_URL_PATH));
                    $tmpPath = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombreZipCdr;
                    @file_put_contents($tmpPath, @file_get_contents($json['ruta_cdr_zip']));
                    if (is_file($tmpPath))
                        $zipCdrLocal = $tmpPath;
                }
            }

            // URLs públicas (si tienes constante URL configurada)
            $zipCpeUrl = ($zipCpeLocal && $url_publica_entorno) ? $url_publica_entorno . basename($zipCpeLocal) : null;
            $zipCdrUrl = ($zipCdrLocal && $url_publica_entorno) ? $url_publica_entorno . basename($zipCdrLocal) : null;

            // === 5) ACTUALIZAR ESTADO EN BD ===
            $estadoEnvio = $okApi ? 1 : 0;
            $nombreZipCpeForDb = $nombreZipCpe ?: null;

            $respUpdate = $dataModel->setDataDocumento(
                $id_venta,
                $estadoEnvio,               // 1=OK, 0=error (ajusta a tu semántica)
                $http,                      // código HTTP devuelto por la API
                $mensaje ?? 'sin mensaje',  // mensaje SUNAT/PSE
                $nombreZipCpeForDb,         // nombre del CPE ZIP (si hubo)
                $hashCdr,                   // hash CDR (si hubo)
                $hashCpe,                   // hash CPE (si hubo)
                "a"                         // tu flag interno
            );

            if (!$respUpdate) {
                throw new \RuntimeException('No se pudo actualizar tm_venta (execute() falló).');
            }

            // === 6) RESPUESTA FINAL ===
            if (!$okApi) {
                return [
                    'ok' => false,
                    'http' => $http,
                    'mensaje' => $mensaje ?? ($json['mensaje'] ?? 'Error del PSE'),
                    'request_json' => $resp['request_json'] ?? null,
                    'response_raw' => $resp['body'] ?? null,
                    'response_json' => $json,
                    'dir_entorno' => $dir_entorno,
                ];
            }

            return [
                'ok' => true,
                'http' => $http,
                'mensaje' => $mensaje,
                'hash_cpe' => $hashCpe,
                'hash_cdr' => $hashCdr,
                'name_zip_cpe' => $nombreZipCpe,
                'name_zip_cdr' => $nombreZipCdr,
                'zip_cpe_local' => $zipCpeLocal,
                'zip_cdr_local' => $zipCdrLocal,
                'zip_cpe_url' => $zipCpeUrl,
                'zip_cdr_url' => $zipCdrUrl,
                'rutas_api' => [
                    'ruta_xml' => $json['ruta_xml'] ?? null,
                    'ruta_cdr' => $json['ruta_cdr'] ?? null,
                    'ruta_pdf' => $json['ruta_pdf'] ?? null,
                    'ruta_cpe_zip' => $json['ruta_cpe_zip'] ?? null,
                    'ruta_cdr_zip' => $json['ruta_cdr_zip'] ?? null,
                ],
                'dir_entorno' => $dir_entorno,
            ];

        } catch (\Throwable $e) {
            // Intenta registrar el error en BD
            try {
                $dataModel->setDataDocumento($id_venta, 0, 0, $e->getMessage(), null, null, null, "e");
            } catch (\Throwable $ignore) {
            }

            return ['ok' => false, 'error' => $e->getMessage()];
        }
    }

    public function comunicacionBaja(int $id_venta): array
    {
        if ($id_venta <= 0) {
            return ['ok' => false, 'error' => 'id_venta inválido'];
        }

        $dataModel = new PseModel();
        $apiUrl = 'https://facturalahoy.com/api/facturalaya/comunicacion_baja';

        // 1) Payload
        $payload = $dataModel->getDataComprobanteBaja($id_venta);
        if (!$payload) {
            return ['ok' => false, 'error' => 'No se encontró data de baja para la venta'];
        }

        // 2) Validación negocio
        if (!$dataModel->validarBaja($id_venta)) {
            return ['ok' => false, 'error' => 'La venta no es válida para baja'];
        }

        // 3) Empresa / ambiente
        $empresa = $dataModel->getDataEmpresa();
        if (!$empresa || empty($empresa['ruc'])) {
            return ['ok' => false, 'error' => 'No se encontró la empresa o falta RUC'];
        }
        $ambiente = ((int) $empresa['modo'] === 1) ? 'produccion' : 'prueba';

        // 4) Rutas de salida
        $baseFacturacion = realpath(dirname(__DIR__)) ?: dirname(__DIR__);
        $rucEmisor = preg_replace('/\D/', '', (string) ($payload['emisor']['ruc'] ?? $empresa['ruc']));
        $dir_entorno = rtrim($baseFacturacion, '/\\') . "/UBL21/archivos_xml_sunat/comunicacion_bajas/{$ambiente}/{$rucEmisor}/";
        $url_publica_entorno = rtrim(URL, '/') . "/facturacion/UBL21/archivos_xml_sunat/comunicacion_bajas/{$ambiente}/{$rucEmisor}/";

        // Helper guardar base64
        $guardarZipB64 = function (?string $b64, string $nombre) use ($dir_entorno): ?string {
            if (!$b64)
                return null;
            $bin = base64_decode($b64, true);
            if ($bin === false)
                throw new \RuntimeException("Base64 inválido para $nombre");
            if (!is_dir($dir_entorno) && !mkdir($dir_entorno, 0775, true) && !is_dir($dir_entorno)) {
                throw new \RuntimeException('No se pudo crear la carpeta: ' . $dir_entorno);
            }
            $path = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombre;
            if (file_put_contents($path, $bin) === false) {
                throw new \RuntimeException("No se pudo guardar $nombre en $path");
            }
            return $path;
        };

        try {

            // 5) Enviar (usa tu mismo cURL JSON)
            $resp = $this->enviarFacturaApi($apiUrl, $payload, [], 90, false);

            $http = (int) ($resp['status'] ?? 0);
            if ($http < 200 || $http >= 300) {
                // No lances excepción: devuelve detalle
                return [
                    'ok' => false,
                    'http' => $http,
                    'message' => $resp['body'] ?? 'Error HTTP en comunicación de baja',
                    'response_json' => $resp['json'] ?? null
                ];
            }

            $json = $resp['json'] ?? null;
            if (!is_array($json)) {
                return ['ok' => false, 'http' => $http, 'message' => 'Respuesta no-JSON en comunicación de baja', 'raw' => $resp['body'] ?? null];
            }

            if (($json['respuesta'] ?? '') !== 'ok') {
                $detalle = $json['msj_sunat'] ?? $json['mensaje'] ?? 'sin mensaje';
                // Graba estado error en BD
                $dataModel->setDataDocumento($id_venta, 0, $http, $detalle, null, null, null, "i");
                return ['ok' => false, 'http' => $http, 'message' => $detalle, 'response_json' => $json];
            }

            // 6) Extraer campos
            $mensaje = $json['resp_sunat']['mensaje'] ?? ($json['msj_sunat'] ?? ($json['mensaje'] ?? null));
            $hashCdr = $json['resp_sunat']['hash_cdr'] ?? ($json['hash_cdr'] ?? null);
            $hashCpe = $json['hash_cpe'] ?? null;
            $ticket = $json['codigo_ticket'] ?? $json['ticket'] ?? null;
            $codSunat = $json['cod_sunat'] ?? null;

            $nombreZipCpe = $json['name_file_zip_cpe'] ?? null; // RA-CPE.zip si viene
            $nombreZipCdr = $json['name_file_zip_cdr'] ?? null; // RA-CDR.zip si viene

            // 7) ¿Vino resultado final (zip) o solo ticket?
            $zipCpeLocal = null;
            $zipCdrLocal = null;
            $zipCpeUrl = null;
            $zipCdrUrl = null;

            $hayArchivos = !empty($json['file_cpe_zip']) || !empty($json['file_cdr_zip']) || !empty($json['ruta_cpe_zip']) || !empty($json['ruta_cdr_zip']);

            if ($hayArchivos) {
                // Guardar si vinieron en base64
                if (!empty($json['file_cpe_zip']) && $nombreZipCpe) {
                    $zipCpeLocal = $guardarZipB64($json['file_cpe_zip'], $nombreZipCpe);
                }
                if (!empty($json['file_cdr_zip']) && $nombreZipCdr) {
                    $zipCdrLocal = $guardarZipB64($json['file_cdr_zip'], $nombreZipCdr);
                }

                // Fallback por URL (si tu PSE las da)
                if (!$zipCpeLocal && !empty($json['ruta_cpe_zip'])) {
                    $nombreZipCpe = $nombreZipCpe ?: basename(parse_url($json['ruta_cpe_zip'], PHP_URL_PATH));
                    $tmp = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombreZipCpe;
                    @file_put_contents($tmp, @file_get_contents($json['ruta_cpe_zip']));
                    if (is_file($tmp))
                        $zipCpeLocal = $tmp;
                }
                if (!$zipCdrLocal && !empty($json['ruta_cdr_zip'])) {
                    $nombreZipCdr = $nombreZipCdr ?: basename(parse_url($json['ruta_cdr_zip'], PHP_URL_PATH));
                    $tmp = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombreZipCdr;
                    @file_put_contents($tmp, @file_get_contents($json['ruta_cdr_zip']));
                    if (is_file($tmp))
                        $zipCdrLocal = $tmp;
                }

                $zipCpeUrl = ($zipCpeLocal && $url_publica_entorno) ? $url_publica_entorno . basename($zipCpeLocal) : null;
                $zipCdrUrl = ($zipCdrLocal && $url_publica_entorno) ? $url_publica_entorno . basename($zipCdrLocal) : null;

                // 8-A) Resultado final → actualiza como OK
                $dataModel->setDataDocumento($id_venta, 1, $http, $mensaje ?? 'Baja aceptada', $nombreZipCpe, $hashCdr, $hashCpe, "i");

                return [
                    'ok' => true,
                    'final' => true,
                    'http' => $http,
                    'ticket' => $ticket,
                    'cod_sunat' => $codSunat,
                    'mensaje' => $mensaje,
                    'hash_cpe' => $hashCpe,
                    'hash_cdr' => $hashCdr,
                    'dir_entorno' => $dir_entorno,
                    'zip_cpe_local' => $zipCpeLocal,
                    'zip_cdr_local' => $zipCdrLocal,
                    'zip_cpe_url' => $zipCpeUrl,
                    'zip_cdr_url' => $zipCdrUrl,
                    'nombres_zip' => ['cpe' => $nombreZipCpe, 'cdr' => $nombreZipCdr],
                ];

            } else {
                // 8-B) Solo ticket → deja en PENDIENTE
                // Usa '2' como estado pendiente (ajústalo a tu semántica)
                $msgPend = 'Baja enviada. Ticket: ' . ($ticket ?: 'N/A');
                $dataModel->setDataDocumento($id_venta, 2, $http, $msgPend, null, null, null, "i");

                return [
                    'ok' => true,
                    'final' => false,
                    'http' => $http,
                    'ticket' => $ticket,
                    'cod_sunat' => $codSunat,
                    'mensaje' => $mensaje ?: $msgPend,
                ];
            }

        } catch (\Throwable $e) {
            return ['ok' => false, 'error' => $e->getMessage()];
        }
    }

    public function resumenBoleta(int $id_venta, int $accion): array
    {
        if ($id_venta <= 0) {
            return ['ok' => false, 'error' => 'id_venta inválido'];
        }

        $dataModel = new PseModel();
        //$apiUrl = 'https://facturalahoy.com/api/comunicacion_baja';
        $apiUrl = 'https://facturalahoy.com/api/facturalayaprueba/resumen_boletas';

        $payload = $dataModel->getDataComprobanteResumen($id_venta, $accion);
        if (!$payload) {
            return ['ok' => false, 'error' => 'No se encontró data de baja para la venta'];
        }

        
        if (!$dataModel->validarBaja($id_venta)) {
            return ['ok' => false, 'error' => 'La venta no es válida para baja'];
        }

    
        $empresa = $dataModel->getDataEmpresa();
        if (!$empresa || empty($empresa['ruc'])) {
            return ['ok' => false, 'error' => 'No se encontró la empresa o falta RUC'];
        }
        $ambiente = ((int) $empresa['modo'] === 1) ? 'produccion' : 'prueba';

        
        $baseFacturacion = realpath(dirname(__DIR__)) ?: dirname(__DIR__);
        $rucEmisor = preg_replace('/\D/', '', (string) ($payload['emisor']['ruc'] ?? $empresa['ruc']));
        $dir_entorno = rtrim($baseFacturacion, '/\\') . "/UBL21/archivos_xml_sunat/comunicacion_bajas/{$ambiente}/{$rucEmisor}/";
        $url_publica_entorno = rtrim(URL, '/') . "/facturacion/UBL21/archivos_xml_sunat/comunicacion_bajas/{$ambiente}/{$rucEmisor}/";

        $guardarZipB64 = function (?string $b64, string $nombre) use ($dir_entorno): ?string {
            if (!$b64)
                return null;
            $bin = base64_decode($b64, true);
            if ($bin === false)
                throw new \RuntimeException("Base64 inválido para $nombre");
            if (!is_dir($dir_entorno) && !mkdir($dir_entorno, 0775, true) && !is_dir($dir_entorno)) {
                throw new \RuntimeException('No se pudo crear la carpeta: ' . $dir_entorno);
            }
            $path = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombre;
            if (file_put_contents($path, $bin) === false) {
                throw new \RuntimeException("No se pudo guardar $nombre en $path");
            }
            return $path;
        };

        try {

            //var_dump($payload);

            
            $resp = $this->enviarFacturaApi($apiUrl, $payload, [], 90, false);

            //var_dump($resp);

            $http = (int) ($resp['status'] ?? 0);
            if ($http < 200 || $http >= 300) {
                return [
                    'ok' => false,
                    'http' => $http,
                    'message' => $resp['body'] ?? 'Error HTTP en comunicación de baja',
                    'response_json' => $resp['json'] ?? null
                ];
            }

            $json = $resp['json'] ?? null;
            if (!is_array($json)) {
                return ['ok' => false, 'http' => $http, 'message' => 'Respuesta no-JSON en comunicación de baja', 'raw' => $resp['body'] ?? null];
            }

            if (($json['respuesta'] ?? '') !== 'ok') {
                $detalle = $json['msj_sunat'] ?? $json['mensaje'] ?? 'sin mensaje';
                $dataModel->setDataDocumento($id_venta, 0, $http, $detalle, null, null, null, "i");
                return ['ok' => false, 'http' => $http, 'message' => $detalle, 'response_json' => $json];
            }

            $mensaje = $json['resp_sunat']['mensaje'] ?? ($json['msj_sunat'] ?? ($json['mensaje'] ?? null));
            $hashCdr = $json['resp_sunat']['hash_cdr'] ?? ($json['hash_cdr'] ?? null);
            $hashCpe = $json['hash_cpe'] ?? null;
            $ticket = $json['codigo_ticket'] ?? $json['ticket'] ?? null;
            $codSunat = $json['cod_sunat'] ?? null;

            $nombreZipCpe = $json['name_file_zip_cpe'] ?? null; // RA-CPE.zip si viene
            $nombreZipCdr = $json['name_file_zip_cdr'] ?? null; // RA-CDR.zip si viene

            $zipCpeLocal = null;
            $zipCdrLocal = null;
            $zipCpeUrl = null;
            $zipCdrUrl = null;

            $hayArchivos = !empty($json['file_cpe_zip']) || !empty($json['file_cdr_zip']) || !empty($json['ruta_cpe_zip']) || !empty($json['ruta_cdr_zip']);

            if ($hayArchivos) {
                // Guardar si vinieron en base64
                if (!empty($json['file_cpe_zip']) && $nombreZipCpe) {
                    $zipCpeLocal = $guardarZipB64($json['file_cpe_zip'], $nombreZipCpe);
                }
                if (!empty($json['file_cdr_zip']) && $nombreZipCdr) {
                    $zipCdrLocal = $guardarZipB64($json['file_cdr_zip'], $nombreZipCdr);
                }

                // Fallback por URL (si tu PSE las da)
                if (!$zipCpeLocal && !empty($json['ruta_cpe_zip'])) {
                    $nombreZipCpe = $nombreZipCpe ?: basename(parse_url($json['ruta_cpe_zip'], PHP_URL_PATH));
                    $tmp = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombreZipCpe;
                    @file_put_contents($tmp, @file_get_contents($json['ruta_cpe_zip']));
                    if (is_file($tmp))
                        $zipCpeLocal = $tmp;
                }
                if (!$zipCdrLocal && !empty($json['ruta_cdr_zip'])) {
                    $nombreZipCdr = $nombreZipCdr ?: basename(parse_url($json['ruta_cdr_zip'], PHP_URL_PATH));
                    $tmp = rtrim($dir_entorno, '/\\') . DIRECTORY_SEPARATOR . $nombreZipCdr;
                    @file_put_contents($tmp, @file_get_contents($json['ruta_cdr_zip']));
                    if (is_file($tmp))
                        $zipCdrLocal = $tmp;
                }

                $zipCpeUrl = ($zipCpeLocal && $url_publica_entorno) ? $url_publica_entorno . basename($zipCpeLocal) : null;
                $zipCdrUrl = ($zipCdrLocal && $url_publica_entorno) ? $url_publica_entorno . basename($zipCdrLocal) : null;

                // 8-A) Resultado final → actualiza como OK
                $dataModel->setDataDocumento($id_venta, 1, $http, $mensaje ?? 'Baja aceptada', $nombreZipCpe, $hashCdr, $hashCpe, "i");

                return [
                    'ok' => true,
                    'final' => true,
                    'http' => $http,
                    'ticket' => $ticket,
                    'cod_sunat' => $codSunat,
                    'mensaje' => $mensaje,
                    'hash_cpe' => $hashCpe,
                    'hash_cdr' => $hashCdr,
                    'dir_entorno' => $dir_entorno,
                    'zip_cpe_local' => $zipCpeLocal,
                    'zip_cdr_local' => $zipCdrLocal,
                    'zip_cpe_url' => $zipCpeUrl,
                    'zip_cdr_url' => $zipCdrUrl,
                    'nombres_zip' => ['cpe' => $nombreZipCpe, 'cdr' => $nombreZipCdr],
                ];

            } else {
                // 8-B) Solo ticket → deja en PENDIENTE
                // Usa '2' como estado pendiente (ajústalo a tu semántica)
                $msgPend = 'Baja enviada. Ticket: ' . ($ticket ?: 'N/A');
                $dataModel->setDataDocumento($id_venta, 2, $http, $msgPend, null, null, null, "i");

                return [
                    'ok' => true,
                    'final' => false,
                    'http' => $http,
                    'ticket' => $ticket,
                    'cod_sunat' => $codSunat,
                    'mensaje' => $mensaje ?: $msgPend,
                ];
            }

        } catch (\Throwable $e) {
            return ['ok' => false, 'error' => $e->getMessage()];
        }
    }

    public function reponerStock($id_pedido)
    {
        // Configurar cabecera JSON
        header('Content-Type: application/json; charset=utf-8');

        // Validar ID
        if (!is_numeric($id_pedido) || $id_pedido <= 0) {
            http_response_code(400);
            echo json_encode([
                'status'  => 'error',
                'message' => 'Parámetro id_pedi$id_pedido inválido',
            ], JSON_UNESCAPED_UNICODE);
            return;
        }

        try {
            // Ejecutar la reposición
            $dataModel = new PseModel();
            $result = $dataModel->reponerStock((int)$id_pedido);

            // Venta ya repuesta o sin detalles
            if (!empty($result['ok']) && !$result['ok']) {
                http_response_code(202);
                echo json_encode([
                    'status'  => 'error',
                    'message' => $result['msg'] ?? 'Sin acción realizada',
                    'data'    => $result,
                ], JSON_UNESCAPED_UNICODE);
                return;
            }

            // OK
            if (!empty($result['ok'])) {
                http_response_code(200);
                echo json_encode([
                    'status'  => 'ok',
                    'message' => $result['message'] ?? 'Stock repuesto correctamente',
                    'data'    => $result,
                ], JSON_UNESCAPED_UNICODE);
                return;
            }

            // Si no entra en ninguno de los casos
            http_response_code(500);
            echo json_encode([
                'status'  => 'error',
                'message' => 'Respuesta inesperada del servicio',
                'data'    => $result,
            ], JSON_UNESCAPED_UNICODE);
        } catch (RuntimeException $e) {
            // Error controlado: venta inexistente u otro error de negocio
            http_response_code(404);
            echo json_encode([
                'status'  => 'error',
                'message' => $e->getMessage(),
            ], JSON_UNESCAPED_UNICODE);
        } catch (Throwable $e) {
            // Error grave o interno
            http_response_code(500);
            echo json_encode([
                'status'  => 'error',
                'message' => 'Error interno al reponer stock',
                'error'   => $e->getMessage(),
            ], JSON_UNESCAPED_UNICODE);
        }
    }

    public function reponerStockVenta($id_venta)
    {
        // Configurar cabecera JSON
        header('Content-Type: application/json; charset=utf-8');

        // Validar ID
        if (!is_numeric($id_venta) || $id_venta <= 0) {
            http_response_code(400);
            echo json_encode([
                'status'  => 'error',
                'message' => 'Parámetro id_venta inválido',
            ], JSON_UNESCAPED_UNICODE);
            return;
        }

        try {
            // Ejecutar la reposición
            $dataModel = new PseModel();
            $result = $dataModel->reponerStockVenta((int)$id_venta);

            // Venta ya repuesta o sin detalles
            if (!empty($result['ok']) && !$result['ok']) {
                http_response_code(202);
                echo json_encode([
                    'status'  => 'error',
                    'message' => $result['msg'] ?? 'Sin acción realizada',
                    'data'    => $result,
                ], JSON_UNESCAPED_UNICODE);
                return;
            }

            // OK
            if (!empty($result['ok'])) {
                http_response_code(200);
                echo json_encode([
                    'status'  => 'ok',
                    'message' => $result['message'] ?? 'Stock repuesto correctamente',
                    'data'    => $result,
                ], JSON_UNESCAPED_UNICODE);
                return;
            }

            // Si no entra en ninguno de los casos
            http_response_code(500);
            echo json_encode([
                'status'  => 'error',
                'message' => 'Respuesta inesperada del servicio',
                'data'    => $result,
            ], JSON_UNESCAPED_UNICODE);
        } catch (RuntimeException $e) {
            // Error controlado: venta inexistente u otro error de negocio
            http_response_code(404);
            echo json_encode([
                'status'  => 'error',
                'message' => $e->getMessage(),
            ], JSON_UNESCAPED_UNICODE);
        } catch (Throwable $e) {
            // Error grave o interno
            http_response_code(500);
            echo json_encode([
                'status'  => 'error',
                'message' => 'Error interno al reponer stock',
                'error'   => $e->getMessage(),
            ], JSON_UNESCAPED_UNICODE);
        }
    }

}

