<?php

namespace App\Services;

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 Illuminate\Support\Facades\Storage;
use Exception;
use Throwable;
use ZipArchive;

class SATCfdiService
{
    private Service $service;

    private const CERT_PATH = 'certs/oicc6107064a4.cer';
    private const KEY_PATH  = 'certs/Claveprivada_FIEL_OICC6107064A4_20250509_114403.key';
    private const PASSWORD  = 'con45130';

    private const DOCUMENT_TYPE_MAP = [
        'ingreso'  => 'ingreso',
        'egreso'   => 'egreso',
        'traslado' => 'traslado',
        'nomina'   => 'nomina',
        'pago'     => 'pago',
    ];

    public function __construct()
    {
        $cerPath = storage_path(self::CERT_PATH);
        $keyPath = storage_path(self::KEY_PATH);

        if (!file_exists($cerPath)) {
            throw new Exception("Archivo CER no encontrado: {$cerPath}");
        }
        if (!file_exists($keyPath)) {
            throw new Exception("Archivo KEY no encontrado: {$keyPath}");
        }

        $fiel = Fiel::create(
            file_get_contents($cerPath),
            file_get_contents($keyPath),
            self::PASSWORD
        );

        Log::info('SAT CFDI: Datos del certificado FIEL cargado', [
            'rfc'            => $fiel->getRfc(),
            'serial'         => $fiel->getCertificateSerial(),
            'issuer'         => $fiel->getCertificateIssuerName(),
            'is_valid'       => $fiel->isValid(),
        ]);

        if (!$fiel->isValid()) {
            throw new Exception("La FIEL no es válida. Verifica que sea una FIEL (no CSD) y que esté vigente.");
        }

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

    /**
     * Crear una solicitud de descarga de CFDIs con filtros opcionales.
     *
     * @param string $fechaInicio       Formato: Y-m-d H:i:s
     * @param string $fechaFin          Formato: Y-m-d H:i:s
     * @param string $tipoComprobante   ingreso|egreso|traslado|nomina|pago (vacío = sin filtro)
     * @param string $tipoSolicitud     xml|metadata
     * @param string $tipoDescarga      emitidos|recibidos
     * @param string $estadoComprobante activo|cancelado|todos
     * @return array
     * @throws Exception
     */
    public function crearSolicitud(
        string $fechaInicio,
        string $fechaFin,
        string $tipoComprobante = 'ingreso',
        string $tipoSolicitud = 'xml',
        string $tipoDescarga = 'emitidos',
        string $estadoComprobante = 'activo'
    ): array {
        $period = DateTimePeriod::createFromValues($fechaInicio, $fechaFin);

        $query = QueryParameters::create()
            ->withPeriod($period)
            ->withDownloadType($tipoDescarga === 'recibidos' ? DownloadType::received() : DownloadType::issued())
            ->withRequestType($tipoSolicitud === 'metadata' ? RequestType::metadata() : RequestType::xml())
            ->withDocumentType($this->resolveDocumentType($tipoComprobante))
            ->withDocumentStatus($this->resolveDocumentStatus($estadoComprobante));

        $errors = $query->validate();
        if ($errors !== []) {
            Log::error('SAT CFDI: Parámetros de consulta inválidos', [
                'errors' => $errors,
                'params' => compact('fechaInicio', 'fechaFin', 'tipoComprobante', 'tipoSolicitud', 'tipoDescarga', 'estadoComprobante'),
            ]);
            throw new Exception("Parámetros de consulta inválidos: " . implode(', ', $errors));
        }

        Log::info('SAT CFDI: Enviando solicitud al SAT', [
            'params' => compact('fechaInicio', 'fechaFin', 'tipoComprobante', 'tipoSolicitud', 'tipoDescarga', 'estadoComprobante'),
        ]);

        try {
            Log::info('SAT CFDI: Paso 1 - Autenticando con el SAT...');
            $token = $this->service->authenticate();
            Log::info('SAT CFDI: Paso 1 - Autenticación completada', [
                'token_is_valid' => $token->isValid(),
                'token_value_preview' => substr($token->getValue(), 0, 40) . '...',
                'token_expires' => $token->getExpires()->format('Y-m-d H:i:s'),
            ]);
            $this->service->setToken($token);

            Log::info('SAT CFDI: Paso 2 - Enviando query al SAT...');
            $result = $this->service->query($query);

            Log::info('SAT CFDI: Respuesta recibida del SAT', [
                'request_id' => $result->getRequestId(),
                'status_code' => $result->getStatus()->getCode(),
                'status_message' => $result->getStatus()->getMessage(),
                'is_accepted' => $result->getStatus()->isAccepted(),
            ]);

            if (!$result->getStatus()->isAccepted()) {
                Log::warning('SAT CFDI: Solicitud rechazada por el SAT', [
                    'status_code' => $result->getStatus()->getCode(),
                    'status_message' => $result->getStatus()->getMessage(),
                ]);
                throw new Exception("El SAT rechazó la solicitud: " . $result->getStatus()->getMessage());
            }
        } catch (WebClientException $e) {
            $previous = $e->getPrevious();
            Log::error('SAT CFDI: Error al comunicarse con el SAT', [
                'exception_class'        => get_class($e),
                'exception_message'      => $e->getMessage(),
                'http_status'            => $e->getResponse()->getStatusCode(),
                'response_body'          => $e->getResponse()->getBody(),
                'response_headers'       => $e->getResponse()->getHeaders(),
                'cause_class'            => $previous ? get_class($previous) : null,
                'cause_message'          => $previous ? $previous->getMessage() : null,
                'params'                 => compact('fechaInicio', 'fechaFin', 'tipoComprobante', 'tipoSolicitud', 'tipoDescarga', 'estadoComprobante'),
            ]);
            $deepCause = $previous ? $previous->getPrevious() : null;
            Log::error('SAT CFDI: Causa raíz del error de conexión', [
                'deep_cause_class'   => $deepCause ? get_class($deepCause) : null,
                'deep_cause_message' => $deepCause ? $deepCause->getMessage() : null,
            ]);
            $causeMsg = $deepCause ? $deepCause->getMessage() : ($previous ? $previous->getMessage() : 'sin detalle');
            throw new Exception("Error de conexión con el SAT: {$causeMsg}");
        } catch (Throwable $e) {
            Log::error('SAT CFDI: Error inesperado', [
                'exception_class'   => get_class($e),
                'exception_message' => $e->getMessage(),
                'exception_trace'   => $e->getTraceAsString(),
                'params'            => compact('fechaInicio', 'fechaFin', 'tipoComprobante', 'tipoSolicitud', 'tipoDescarga', 'estadoComprobante'),
            ]);
            throw $e;
        }

        return [
            'success'          => true,
            'request_id'       => $result->getRequestId(),
            'status_code'      => $result->getStatus()->getCode(),
            'status_message'   => $result->getStatus()->getMessage(),
            'fecha_inicio'     => $fechaInicio,
            'fecha_fin'          => $fechaFin,
            'tipo_comprobante'   => $tipoComprobante,
            'tipo_solicitud'     => $tipoSolicitud,
            'tipo_descarga'      => $tipoDescarga,
            'estado_comprobante' => $estadoComprobante,
        ];
    }

    /**
     * Verificar el estado de una solicitud de descarga.
     *
     * @param string $requestId UUID de la solicitud
     * @return array
     * @throws Exception
     */
    public function verificarSolicitud(string $requestId): array
    {
        try {
            Log::info('SAT CFDI: Verificando solicitud', ['request_id' => $requestId]);

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

            Log::info('SAT CFDI: Estado de solicitud obtenido', [
                'request_id'     => $requestId,
                'status_code'    => $result->getStatus()->getCode(),
                'status_message' => $result->getStatus()->getMessage(),
                'code_request'   => $result->getCodeRequest()->getValue(),
                'number_cfdis'   => $result->getNumberCfdis(),
            ]);

            $packages = [];
            foreach ($result->getPackagesIds() as $packageId) {
                $packages[] = $packageId;
            }

            return [
                'success'         => true,
                'request_id'      => $requestId,
                'status_code'     => $result->getStatus()->getCode(),
                'status_message'  => $result->getStatus()->getMessage(),
                'code_request'    => $result->getCodeRequest()->getValue(),
                'number_cfdis'    => $result->getNumberCfdis(),
                'packages'        => $packages,
            ];
        } catch (WebClientException $e) {
            $previous = $e->getPrevious();
            Log::error('SAT CFDI: Error al verificar solicitud', [
                'exception_class'   => get_class($e),
                'exception_message' => $e->getMessage(),
                'http_status'       => $e->getResponse()->getStatusCode(),
                'response_body'     => $e->getResponse()->getBody(),
                'cause_class'       => $previous ? get_class($previous) : null,
                'cause_message'     => $previous ? $previous->getMessage() : null,
                'request_id'        => $requestId,
            ]);
            $causeMsg = $previous ? $previous->getMessage() : 'sin detalle';
            throw new Exception("Error al verificar solicitud: {$causeMsg}");
        } catch (Throwable $e) {
            Log::error('SAT CFDI: Error inesperado al verificar', [
                'exception_class'   => get_class($e),
                'exception_message' => $e->getMessage(),
                'request_id'        => $requestId,
            ]);
            throw $e;
        }
    }

    /**
     * Descargar un paquete de CFDIs y extraer los XMLs a una carpeta con el nombre del RFC.
     *
     * @param string $packageId ID del paquete
     * @return array
     * @throws Exception
     */
    public function descargarPaquete(string $packageId): array
    {
        try {
            Log::info('SAT CFDI: Descargando paquete', ['package_id' => $packageId]);

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

            Log::info('SAT CFDI: Paquete descargado', [
                'package_id'     => $packageId,
                'status_code'    => $result->getStatus()->getCode(),
                'status_message' => $result->getStatus()->getMessage(),
                'package_size'   => $result->getPackageSize(),
            ]);

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

            // Obtener el RFC del certificado
            $cerPath = storage_path(self::CERT_PATH);
            $fiel = Fiel::create(
                file_get_contents($cerPath),
                file_get_contents(storage_path(self::KEY_PATH)),
                self::PASSWORD
            );
            $rfc = $fiel->getRfc();

            // Crear carpeta para el RFC si no existe
            $rfcFolder = storage_path("app/cfdi/{$rfc}");
            if (!is_dir($rfcFolder)) {
                mkdir($rfcFolder, 0755, true);
            }

            // Guardar ZIP temporalmente
            $tempZipPath = storage_path("app/temp_{$packageId}.zip");
            file_put_contents($tempZipPath, $result->getPackageContent());

            // Extraer XMLs del ZIP
            $zip = new ZipArchive();
            $extractedFiles = [];
            
            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);
                        $extractedFiles[] = $filename;
                    }
                }
                $zip->close();
            } else {
                throw new Exception("No se pudo abrir el archivo ZIP");
            }

            // Eliminar ZIP temporal
            unlink($tempZipPath);

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

            return [
                'success'         => true,
                'package_id'      => $packageId,
                'status_code'     => $result->getStatus()->getCode(),
                'status_message'  => $result->getStatus()->getMessage(),
                'rfc'             => $rfc,
                'folder'          => $rfcFolder,
                'files_extracted' => count($extractedFiles),
                'files'           => $extractedFiles,
            ];
        } catch (WebClientException $e) {
            $previous = $e->getPrevious();
            Log::error('SAT CFDI: Error al descargar paquete', [
                'exception_class'   => get_class($e),
                'exception_message' => $e->getMessage(),
                'http_status'       => $e->getResponse()->getStatusCode(),
                'response_body'     => $e->getResponse()->getBody(),
                'cause_class'       => $previous ? get_class($previous) : null,
                'cause_message'     => $previous ? $previous->getMessage() : null,
                'package_id'        => $packageId,
            ]);
            $causeMsg = $previous ? $previous->getMessage() : 'sin detalle';
            throw new Exception("Error al descargar paquete: {$causeMsg}");
        } catch (Throwable $e) {
            Log::error('SAT CFDI: Error inesperado al descargar', [
                'exception_class'   => get_class($e),
                'exception_message' => $e->getMessage(),
                'package_id'        => $packageId,
            ]);
            throw $e;
        }
    }

    private function resolveDocumentType(string $tipo): DocumentType
    {
        return match ($tipo) {
            'ingreso'  => DocumentType::ingreso(),
            'egreso'   => DocumentType::egreso(),
            'traslado' => DocumentType::traslado(),
            'nomina'   => DocumentType::nomina(),
            'pago'     => DocumentType::pago(),
            default    => DocumentType::undefined(),
        };
    }

    private function resolveDocumentStatus(string $estado): DocumentStatus
    {
        return match ($estado) {
            'activo'    => DocumentStatus::active(),
            'cancelado' => DocumentStatus::cancelled(),
            'todos'     => DocumentStatus::undefined(),
            default     => DocumentStatus::active(),
        };
    }
}
