<?php

namespace WMS\Integrations\Sap;

/**
 * Cliente para SAP Business One Service Layer.
 *
 * Maneja:
 * - Autenticación (login/session)
 * - Requests GET/POST/PATCH
 * - Manejo de sesión con cookies
 * - Reintentos ante errores transitorios
 */
class SapServiceLayerClient
{
    private string $baseUrl;
    private string $companyDb;
    private string $username;
    private string $password;
    private bool $verifySsl;
    private int $timeout;
    private ?string $sessionId = null;
    private ?string $cookieFile = null;

    public function __construct()
    {
        $config = require __DIR__ . '/../../config/app.php';
        $sap = $config['sap'];

        $this->baseUrl    = rtrim($sap['service_layer_url'], '/');
        $this->companyDb  = $sap['company_db'];
        $this->username   = $sap['username'];
        $this->password   = $sap['password'];
        $this->verifySsl  = $sap['verify_ssl'];
        $this->timeout    = $sap['timeout'];
        $this->cookieFile = sys_get_temp_dir() . '/wms_sap_cookies_' . md5($this->baseUrl) . '.txt';
    }

    /**
     * Inicia sesión en SAP Service Layer.
     */
    public function login(): bool
    {
        $response = $this->request('POST', '/Login', [
            'CompanyDB' => $this->companyDb,
            'UserName'  => $this->username,
            'Password'  => $this->password,
        ], false);

        if (isset($response['SessionId'])) {
            $this->sessionId = $response['SessionId'];
            return true;
        }

        throw new \RuntimeException('Error al autenticar con SAP Service Layer: ' . json_encode($response));
    }

    /**
     * Cierra la sesión en SAP.
     */
    public function logout(): void
    {
        try {
            $this->request('POST', '/Logout');
        } catch (\Throwable $e) {
            // Ignorar errores de logout
        }
        $this->sessionId = null;
    }

    // ── MAESTROS ────────────────────────────────────────────────

    /**
     * Obtiene artículos de SAP.
     */
    public function getItems(int $top = 100, int $skip = 0): array
    {
        return $this->get("/Items?\$top={$top}&\$skip={$skip}&\$select=ItemCode,ItemName,BarCode,ItemsGroupCode,InventoryUOM,ManageBatchNumbers,ManageSerialNumbers,U_subgrupo");
    }

    /**
     * Obtiene un artículo por código.
     */
    public function getItem(string $itemCode): array
    {
        return $this->get("/Items('{$itemCode}')");
    }

    /**
     * Obtiene almacenes de SAP.
     */
    public function getWarehouses(): array
    {
        return $this->get("/Warehouses?\$select=WarehouseCode,WarehouseName,Street,City");
    }

    /**
     * Obtiene grupos de artículos (paginado por el caller).
     */
    public function getItemGroups(int $top = 100, int $skip = 0): array
    {
        return $this->get("/ItemGroups?\$top={$top}&\$skip={$skip}&\$select=Number,GroupName");
    }

    /**
     * Obtiene socios de negocio (proveedores/clientes).
     */
    public function getBusinessPartners(string $type = 'S', int $top = 100): array
    {
        return $this->get("/BusinessPartners?\$filter=CardType eq 'cSupplier'&\$top={$top}&\$select=CardCode,CardName,CardType");
    }

    /**
     * Obtiene lotes de un artículo.
     */
    public function getBatchNumbers(string $itemCode): array
    {
        return $this->get("/BatchNumberDetails?\$filter=ItemCode eq '{$itemCode}'&\$select=Batch,ItemCode,ExpiryDate,ManufacturingDate,Status");
    }

    // ── DOCUMENTOS ──────────────────────────────────────────────

    /**
     * Obtiene órdenes de compra abiertas.
     */
    public function getOpenPurchaseOrders(int $top = 500): array
    {
        // SAP Service Layer NO acepta $filter sobre UDFs ("Property X is invalid").
        // Solucion: traemos todas las OCs abiertas y filtramos U_XPertWMS='Y'
        // client-side en PHP. Mantenemos el guard sin fecha para que la marca
        // sea el unico criterio de visibilidad WMS.
        $filter = "DocumentStatus eq 'bost_Open'";
        $filterEnc = rawurlencode($filter);
        $r = $this->get("/PurchaseOrders?\$filter={$filterEnc}&\$top={$top}&\$orderby=DocDate desc");
        $all = $r['value'] ?? [];
        // El UDF U_XPertWMS puede no existir en la CompanyDB (ej. TEST2805 no lo tiene).
        // Si existe en los documentos -> filtramos por 'Y'. Si no existe -> devolvemos todas.
        $hasUdf = false;
        foreach ($all as $po) { if (array_key_exists('U_XPertWMS', $po)) { $hasUdf = true; break; } }
        if ($hasUdf) {
            $only = array_values(array_filter($all, fn($po) => ($po['U_XPertWMS'] ?? 'N') === 'Y'));
        } else {
            $only = $all;
        }
        return ['value' => $only];
    }

    /**
     * Obtiene una orden de compra por DocEntry.
     */
    public function getPurchaseOrder(int $docEntry): array
    {
        return $this->get("/PurchaseOrders({$docEntry})");
    }

    /**
     * Obtiene pedidos de venta abiertos.
     */
    public function getOpenSalesOrders(int $top = 50): array
    {
        return $this->get("/Orders?\$filter=DocumentStatus eq 'bost_Open'&\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * Obtiene un pedido de venta por DocEntry.
     */
    public function getSalesOrder(int $docEntry): array
    {
        return $this->get("/Orders({$docEntry})");
    }

    /**
     * Crea una Entrada de Mercadería (Goods Receipt PO) en SAP.
     */
    public function createGoodsReceiptPO(array $data): array
    {
        return $this->post('/PurchaseDeliveryNotes', $data);
    }

    /**
     * Crea una Entrega (Delivery) en SAP.
     */
    public function createDelivery(array $data): array
    {
        return $this->post('/DeliveryNotes', $data);
    }

    /**
     * Crea un movimiento de mercadería en SAP (transferencia de stock).
     */
    /**
     * Obtiene Notas de Crédito abiertas.
     */
    public function getOpenCreditNotes(int $top = 50): array
    {
        return $this->get("/CreditNotes?\$filter=DocumentStatus eq 'bost_Open'&\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * Obtiene Transferencias de Stock.
     */
    public function getStockTransfers(int $top = 50): array
    {
        return $this->get("/StockTransfers?\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * Obtiene Solicitudes de Traslado abiertas.
     */
    public function getTransferRequests(int $top = 50, ?string $toWarehouse = 'CEN'): array
    {
        // Por defecto el WMS solo procesa STs cuyo destino sea CEN (el almacen
        // que administramos). Pasar $toWarehouse = null para listar todas.
        $filter = "DocumentStatus eq 'bost_Open'";
        if ($toWarehouse !== null && $toWarehouse !== '') {
            $whEscaped = str_replace("'", "''", $toWarehouse);
            $filter .= " and ToWarehouse eq '{$whEscaped}'";
        }
        $filterEncoded = rawurlencode($filter);
        return $this->get("/InventoryTransferRequests?\$filter={$filterEncoded}&\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * Obtiene una Transferencia de Stock por DocEntry (incluye DocumentLines).
     */
    public function getStockTransfer(int $docEntry): array
    {
        return $this->get("/StockTransfers({$docEntry})");
    }

    /**
     * Obtiene una Solicitud de Traslado por DocEntry (incluye DocumentLines).
     */
    public function getTransferRequest(int $docEntry): array
    {
        return $this->get("/InventoryTransferRequests({$docEntry})");
    }

    /**
     * Obtiene Entregas (Delivery Notes).
     */
    public function getDeliveryNotes(int $top = 50): array
    {
        return $this->get("/DeliveryNotes?\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * Obtiene Entradas de Mercadería.
     */
    /**
     * Obtiene Ordenes de Produccion.
     */
    
    /**
     * Obtiene Facturas de Deudores (AR Invoices).
     */
    public function getInvoices(int $top = 50): array
    {
        return $this->get("/Invoices?\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * Obtiene Entradas de Inventario (Goods Receipt).
     */
    public function getInventoryEntries(int $top = 50): array
    {
        return $this->get("/InventoryGenEntries?\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * Obtiene Salidas de Inventario (Goods Issue).
     */
    public function getInventoryExits(int $top = 50): array
    {
        return $this->get("/InventoryGenExits?\$top={$top}&\$orderby=DocDate desc");
    }
    public function getProductionOrders(int $top = 50): array
    {
        return $this->get("/ProductionOrders?\$filter=ProductionOrderStatus eq 'boposReleased' or ProductionOrderStatus eq 'boposPlanned'&\$top={$top}&\$orderby=CreationDate desc");
    }

        public function getGoodsReceiptPO(int $top = 50): array
    {
        return $this->get("/PurchaseDeliveryNotes?\$top={$top}&\$orderby=DocDate desc");
    }

        public function createStockTransfer(array $data): array
    {
        return $this->post('/StockTransfers', $data);
    }

    /**
     * Crea un ajuste de inventario en SAP.
     */
    public function createInventoryCountingAdjustment(array $data): array
    {
        return $this->post('/InventoryGenEntries', $data);
    }

    /**
     * Crea una Nota de Crédito en SAP.
     */
    public function createCreditNote(array $data): array
    {
        return $this->post('/CreditNotes', $data);
    }

    /**
     * Obtiene notas de crédito de SAP.
     */
    public function getCreditNotes(int $top = 20): array
    {
        return $this->get("/CreditNotes?\$top={$top}&\$orderby=DocDate desc");
    }

    // ── HTTP ────────────────────────────────────────────────────

    public function get(string $endpoint): array
    {
        return $this->request('GET', $endpoint);
    }

    public function post(string $endpoint, array $data): array
    {
        return $this->request('POST', $endpoint, $data);
    }

    public function patch(string $endpoint, array $data): array
    {
        return $this->request('PATCH', $endpoint, $data);
    }

    /**
     * Ejecuta un request HTTP contra SAP Service Layer.
     * Reintenta login si la sesión expiró.
     */
    private function request(string $method, string $endpoint, ?array $data = null, bool $withAuth = true): array
    {
        if ($withAuth && !$this->sessionId) {
            $this->login();
        }

        $url = $this->baseUrl . $endpoint;
        // Encode spaces in URL for OData filters
        $url = str_replace(' ', '%20', $url);

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => $this->timeout,
            CURLOPT_SSL_VERIFYPEER => $this->verifySsl,
            CURLOPT_SSL_VERIFYHOST => $this->verifySsl ? 2 : 0,
            CURLOPT_COOKIEFILE     => $this->cookieFile,
            CURLOPT_COOKIEJAR      => $this->cookieFile,
        ]);

        $headers = [
            'Content-Type: application/json',
            'Prefer: odata.maxpagesize=500',
        ];
        if ($withAuth && $this->sessionId) {
            $headers[] = 'Cookie: B1SESSION=' . $this->sessionId;
        }

        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            if ($data) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            }
        } elseif ($method === 'PATCH') {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
            if ($data) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            }
        }

        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error    = curl_error($ch);
        curl_close($ch);

        if ($error) {
            $this->logSapDown($endpoint, $method, $error, 0, 'CURL_ERROR');
            throw new \RuntimeException("Error de conexión con SAP: {$error}");
        }

        $result = json_decode($response, true) ?? [];

        // Si la sesión expiró, reintentar una vez
        if ($httpCode === 401 && $withAuth) {
            $this->login();
            return $this->request($method, $endpoint, $data, true);
        }

        if ($httpCode >= 500) {
            // Service Layer caído / saturado
            $this->logSapDown($endpoint, $method, $result['error']['message']['value'] ?? '', $httpCode, 'HTTP_5XX');
        }

        if ($httpCode >= 400) {
            $errorMsg = $result['error']['message']['value'] ?? "HTTP {$httpCode}";
            throw new \RuntimeException("Error SAP Service Layer: {$errorMsg}");
        }

        return $result;
    }

    /**
     * Sube un archivo a SAP Service Layer /Attachments2 y devuelve AbsoluteEntry.
     * Usado para adjuntar PDFs (acta de recepción, CoA, etc.) a documentos SAP.
     */
    public function uploadAttachment(string $filePath, ?string $fileNameOverride = null): int
    {
        if (!$this->sessionId) $this->login();
        if (!file_exists($filePath)) throw new \RuntimeException("Archivo no existe: {$filePath}");

        $fileName = $fileNameOverride ?: basename($filePath);
        $mime = mime_content_type($filePath) ?: 'application/octet-stream';

        $url = $this->baseUrl . '/Attachments2';
        $ch  = curl_init();
        $cfile = new \CURLFile($filePath, $mime, $fileName);
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => $this->timeout * 2,
            CURLOPT_SSL_VERIFYPEER => $this->verifySsl,
            CURLOPT_SSL_VERIFYHOST => $this->verifySsl ? 2 : 0,
            CURLOPT_COOKIEFILE     => $this->cookieFile,
            CURLOPT_COOKIEJAR      => $this->cookieFile,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => ['files' => $cfile],
            CURLOPT_HTTPHEADER     => ['Cookie: B1SESSION=' . $this->sessionId],
        ]);
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($err) throw new \RuntimeException("Error de conexión con SAP /Attachments2: {$err}");
        $data = json_decode($resp, true) ?: [];
        if ($code === 401) {
            // re-login y retry una sola vez
            $this->login();
            return $this->uploadAttachment($filePath, $fileNameOverride);
        }
        if ($code >= 400) {
            $errorMsg = $data['error']['message']['value'] ?? "HTTP {$code}";
            throw new \RuntimeException("Error SAP /Attachments2: {$errorMsg}");
        }
        $absEntry = (int)($data['AbsoluteEntry'] ?? 0);
        if ($absEntry <= 0) throw new \RuntimeException("SAP no devolvió AbsoluteEntry: " . substr($resp, 0, 200));
        return $absEntry;
    }

    /**
     * Registra evento SAP_SL_DOWN en audit_log con throttle de 60s
     * (no más de 1 evento del mismo tipo por minuto para evitar spam).
     */
    private function logSapDown(string $endpoint, string $method, string $errorMsg, int $httpCode, string $kind): void
    {
        try {
            $db = \WMS\Core\Database::getConnection();
            // Throttle: si ya hubo SAP_SL_DOWN del mismo kind en últimos 60s, no duplicar
            $stmt = $db->prepare(
                "SELECT 1 FROM audit_log
                 WHERE table_name = 'system' AND action = 'SAP_SL_DOWN'
                   AND created_at >= (NOW() - INTERVAL 60 SECOND)
                   AND new_values LIKE ?
                 LIMIT 1"
            );
            $stmt->execute(['%"kind":"' . $kind . '"%']);
            if ($stmt->fetchColumn()) return;

            $userId = null;
            try { $userId = \WMS\Middleware\AuthMiddleware::userId(); } catch (\Throwable $e) {}

            \WMS\Services\AuditService::log('system', 0, 'SAP_SL_DOWN', null, [
                'kind'      => $kind,                     // CURL_ERROR | HTTP_5XX
                'endpoint'  => $endpoint,
                'method'    => $method,
                'http_code' => $httpCode,
                'error'     => substr($errorMsg, 0, 500),
                'company_db'=> $this->companyDb,
                'sl_url'    => $this->baseUrl,
            ], $userId);
        } catch (\Throwable $e) {
            error_log('[SapServiceLayerClient] No se pudo loguear SAP_SL_DOWN: ' . $e->getMessage());
        }
    }
}
