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

    /**
     * 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 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
    {
        $sinceDate = date('Y-m-d', strtotime('-7 days'));
        return $this->get("/PurchaseOrders?\$filter=DocumentStatus eq 'bost_Open' and DocDate ge '{$sinceDate}'&\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * 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): array
    {
        return $this->get("/InventoryTransferRequests?\$filter=DocumentStatus eq 'bost_Open'&\$top={$top}&\$orderby=DocDate desc");
    }

    /**
     * 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;
    }

    /**
     * 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());
        }
    }
}
