<?php

namespace App\Services;

use App\Models\CertificadoFIEL;
use PhpCfdi\SatWsDescargaMasiva\Service;
use PhpCfdi\SatWsDescargaMasiva\WebClient\GuzzleWebClient;
use GuzzleHttp\Client as GuzzleClient;
use PhpCfdi\SatWsDescargaMasiva\RequestBuilder\FielRequestBuilder\Fiel;
use PhpCfdi\SatWsDescargaMasiva\RequestBuilder\FielRequestBuilder\FielRequestBuilder;
use PhpCfdi\SatWsDescargaMasiva\Shared\DateTimePeriod;
use PhpCfdi\SatWsDescargaMasiva\Services\Query\QueryParameters;
use PhpCfdi\SatWsDescargaMasiva\Shared\DocumentType;
use PhpCfdi\SatWsDescargaMasiva\Shared\DocumentStatus;
use PhpCfdi\SatWsDescargaMasiva\Shared\DownloadType;
use PhpCfdi\SatWsDescargaMasiva\Shared\RequestType;
use PhpCfdi\SatWsDescargaMasiva\WebClient\Exceptions\WebClientException;
use Illuminate\Support\Facades\Log;
use Exception;
use Throwable;
use ZipArchive;

class SATCfdiProgramadoService
{
    private Service $service;
    private string $rfc;

    /**
     * Inicializar el servicio con certificado dinámico desde la base de datos.
     *
     * @param int $idProveedor
     * @throws Exception
     */
    public function __construct(int $idProveedor)
    {
        $certificado = CertificadoFIEL::where('id_proveedor', $idProveedor)->first();

        if (!$certificado) {
            throw new Exception("No se encontró certificado FIEL para el proveedor ID: {$idProveedor}");
        }

        $certPath = config('sat.cert_fiel_path');
        $cerFile = "{$certPath}/{$certificado->cerFile}";
        $keyFile = "{$certPath}/{$certificado->keyFile}";

        if (!file_exists($cerFile)) {
            throw new Exception("Archivo de certificado no encontrado: {$cerFile}");
        }

        if (!file_exists($keyFile)) {
            throw new Exception("Archivo de llave privada no encontrado: {$keyFile}");
        }

        $fiel = Fiel::create(
            file_get_contents($cerFile),
            file_get_contents($keyFile),
            $certificado->password
        );

        Log::info('SAT CFDI Programado: Certificado FIEL cargado', [
            'id_proveedor' => $idProveedor,
            'rfc'          => $fiel->getRfc(),
            'serial'       => $fiel->getCertificateSerial(),
            'is_valid'     => $fiel->isValid(),
        ]);

        if (!$fiel->isValid()) {
            throw new Exception("La FIEL no es válida para el proveedor ID: {$idProveedor}");
        }

        $this->rfc = $fiel->getRfc();

        $sslVerify = config('sat.ssl_verify', true);
        $guzzleClient = new GuzzleClient(['verify' => $sslVerify]);
        $this->service = new Service(new FielRequestBuilder($fiel), new GuzzleWebClient($guzzleClient));
    }

    /**
     * Crear solicitud de descarga para un mes específico.
     *
     * @param int $anio
     * @param int $mes
     * @param string $tipo 'recibido' o 'emitido'
     * @return array
     * @throws Exception
     */
    public function crearSolicitudMensual(int $anio, int $mes, string $tipo): array
    {
        try {
            // Calcular primer y último día del mes
            $fechaInicio = sprintf('%04d-%02d-01 00:00:00', $anio, $mes);
            $ultimoDia = cal_days_in_month(CAL_GREGORIAN, $mes, $anio);
            $fechaFin = sprintf('%04d-%02d-%02d 23:59:59', $anio, $mes, $ultimoDia);

            $period = DateTimePeriod::createFromValues($fechaInicio, $fechaFin);

            $query = QueryParameters::create()
                ->withPeriod($period)
                ->withDownloadType($tipo === 'recibido' ? DownloadType::received() : DownloadType::issued())
                ->withRequestType(RequestType::xml())
                ->withDocumentType(DocumentType::undefined())
                ->withDocumentStatus(DocumentStatus::active());

            $errors = $query->validate();
            if ($errors !== []) {
                throw new Exception("Parámetros de consulta inválidos: " . implode(', ', $errors));
            }

            Log::info('SAT CFDI Programado: Creando solicitud', [
                'anio' => $anio,
                'mes'  => $mes,
                'tipo' => $tipo,
                'rfc'  => $this->rfc,
            ]);

            $token = $this->service->authenticate();
            $this->service->setToken($token);

            $result = $this->service->query($query);

            Log::info('SAT CFDI Programado: Solicitud creada', [
                'request_id'     => $result->getRequestId(),
                'status_code'    => $result->getStatus()->getCode(),
                'status_message' => $result->getStatus()->getMessage(),
            ]);

            if (!$result->getStatus()->isAccepted()) {
                throw new Exception("El SAT rechazó la solicitud: " . $result->getStatus()->getMessage());
            }

            return [
                'success'        => true,
                'request_id'     => $result->getRequestId(),
                'status_code'    => $result->getStatus()->getCode(),
                'status_message' => $result->getStatus()->getMessage(),
                'rfc'            => $this->rfc,
            ];
        } catch (WebClientException $e) {
            $previous = $e->getPrevious();
            $deepCause = $previous ? $previous->getPrevious() : null;
            $causeMsg = $deepCause ? $deepCause->getMessage() : ($previous ? $previous->getMessage() : 'sin detalle');

            Log::error('SAT CFDI Programado: Error al crear solicitud', [
                'exception_message' => $e->getMessage(),
                'cause_message'     => $causeMsg,
                'anio'              => $anio,
                'mes'               => $mes,
                'tipo'              => $tipo,
            ]);

            throw new Exception("Error de conexión con el SAT: {$causeMsg}");
        } catch (Throwable $e) {
            Log::error('SAT CFDI Programado: Error inesperado', [
                'exception_class'   => get_class($e),
                'exception_message' => $e->getMessage(),
            ]);
            throw $e;
        }
    }

    public function getRfc(): string
    {
        return $this->rfc;
    }

    /**
     * Verificar el estado de una solicitud y descargar si está lista.
     *
     * @param string $requestId UUID de la solicitud del SAT
     * @param array $alreadyDownloaded Paquetes ya descargados para omitir
     * @return array
     * @throws Exception
     */
    public function verificarYDescargar(string $requestId, array $alreadyDownloaded = []): array
    {
        try {
            Log::info('SAT CFDI Programado: Verificando solicitud', [
                'request_id' => $requestId,
                'rfc'        => $this->rfc,
            ]);

            $result = $this->service->verify($requestId);

            $codeRequest = $result->getCodeRequest()->getValue();
            $statusMessage = $result->getCodeRequest()->getMessage();

            Log::info('SAT CFDI Programado: Estado obtenido', [
                'request_id'     => $requestId,
                'code_request'   => $codeRequest,
                'status_message' => $statusMessage,
                'number_cfdis'   => $result->getNumberCfdis(),
            ]);

            // Manejar diferentes códigos de estado
            switch ($codeRequest) {
                case 5000: // Solicitud aceptada, paquetes listos
                    $packageIds = $result->getPackagesIds();
                    
                    if (empty($packageIds)) {
                        return [
                            'status'         => 'por_procesar',
                            'code_request'   => $codeRequest,
                            'status_message' => $statusMessage,
                            'numero_cfdis'   => 0,
                            'package_ids'    => [],
                            'archivos_extraidos' => 0,
                        ];
                    }

                    // Descargar y extraer paquetes pendientes
                    $totalArchivos = 0;
                    foreach ($packageIds as $packageId) {
                        if (!in_array($packageId, $alreadyDownloaded)) {
                            $archivos = $this->descargarYExtraerPaquete($packageId);
                            $totalArchivos += $archivos;
                        } else {
                            Log::info('SAT CFDI Programado: Omitiendo paquete ya descargado', [
                                'package_id' => $packageId,
                            ]);
                        }
                    }

                    return [
                        'status'             => 'por_procesar',
                        'code_request'       => $codeRequest,
                        'status_message'     => $statusMessage,
                        'numero_cfdis'       => $result->getNumberCfdis(),
                        'package_ids'        => $packageIds,
                        'archivos_extraidos' => $totalArchivos,
                        'ruta_xml'           => config('sat.xml_storage_path') . '/' . $this->rfc,
                    ];

                case 5004: // No se encontró información (sin CFDIs en el periodo)
                    return [
                        'status'         => 'por_procesar',
                        'code_request'   => $codeRequest,
                        'status_message' => $statusMessage,
                        'numero_cfdis'   => 0,
                        'package_ids'    => [],
                        'archivos_extraidos' => 0,
                    ];

                case 5002: // Se agotaron las solicitudes
                case 5003: // Tope máximo excedido
                case 5005: // Solicitud duplicada
                    throw new \RuntimeException("SAT rechazó la solicitud [{$codeRequest}]: {$statusMessage}");

                default: // En proceso o estado desconocido
                    return [
                        'status'         => 'en_proceso',
                        'code_request'   => $codeRequest,
                        'status_message' => $statusMessage,
                        'numero_cfdis'   => $result->getNumberCfdis(),
                        'package_ids'    => [],
                    ];
            }

        } catch (WebClientException $e) {
            $previous = $e->getPrevious();
            $causeMsg = $previous ? $previous->getMessage() : 'sin detalle';

            Log::error('SAT CFDI Programado: Error al verificar', [
                'exception_message' => $e->getMessage(),
                'cause_message'     => $causeMsg,
                'request_id'        => $requestId,
            ]);

            throw new Exception("Error de conexión con el SAT: {$causeMsg}");
        } catch (Throwable $e) {
            Log::error('SAT CFDI Programado: Error inesperado en verificación', [
                'exception_class'   => get_class($e),
                'exception_message' => $e->getMessage(),
                'request_id'        => $requestId,
            ]);
            throw $e;
        }
    }

    /**
     * Descargar un paquete y extraer los XMLs.
     *
     * @param string $packageId ID del paquete
     * @return int Número de archivos extraídos
     * @throws Exception
     */
    private function descargarYExtraerPaquete(string $packageId): int
    {
        try {
            Log::info('SAT CFDI Programado: Descargando paquete', [
                'package_id' => $packageId,
                'rfc'        => $this->rfc,
            ]);

            $result = $this->service->download($packageId);

            if (!$result->getStatus()->isAccepted()) {
                throw new Exception("El SAT rechazó la descarga del paquete: " . $result->getStatus()->getMessage());
            }

            // Crear carpeta temporal si no existe
            $tempPath = config('sat.temp_downloads_path');
            if (!is_dir($tempPath)) {
                mkdir($tempPath, 0755, true);
            }

            // Guardar ZIP temporal con RFC y timestamp
            $timestamp = now()->format('YmdHis');
            $tempZipPath = "{$tempPath}/{$this->rfc}_{$timestamp}_{$packageId}.zip";
            file_put_contents($tempZipPath, $result->getPackageContent());

            Log::info('SAT CFDI Programado: Paquete guardado temporalmente', [
                'temp_path'    => $tempZipPath,
                'package_size' => $result->getPackageSize(),
            ]);

            // Crear carpeta para XMLs del RFC
            $xmlBasePath = config('sat.xml_storage_path');
            $rfcFolder = "{$xmlBasePath}/{$this->rfc}";
            if (!is_dir($rfcFolder)) {
                mkdir($rfcFolder, 0755, true);
            }

            // Extraer XMLs del ZIP
            $zip = new ZipArchive();
            $extractedCount = 0;

            if ($zip->open($tempZipPath) === true) {
                for ($i = 0; $i < $zip->numFiles; $i++) {
                    $filename = $zip->getNameIndex($i);
                    
                    // Solo extraer archivos XML
                    if (pathinfo($filename, PATHINFO_EXTENSION) === 'xml') {
                        $content = $zip->getFromIndex($i);
                        $destinationPath = "{$rfcFolder}/{$filename}";
                        file_put_contents($destinationPath, $content);
                        $extractedCount++;
                    }
                }
                $zip->close();
            } else {
                throw new Exception("No se pudo abrir el archivo ZIP: {$tempZipPath}");
            }

            // Eliminar ZIP temporal
            unlink($tempZipPath);

            Log::info('SAT CFDI Programado: XMLs extraídos', [
                'package_id'      => $packageId,
                'rfc'             => $this->rfc,
                'folder'          => $rfcFolder,
                'files_extracted' => $extractedCount,
            ]);

            return $extractedCount;

        } catch (Throwable $e) {
            Log::error('SAT CFDI Programado: Error al descargar/extraer paquete', [
                'exception_class'   => get_class($e),
                'exception_message' => $e->getMessage(),
                'package_id'        => $packageId,
            ]);
            throw $e;
        }
    }
}
