<?php

namespace WMS\Integrations\Sap;

use WMS\Core\Database;
use WMS\Repositories\ItemRepository;
use WMS\Repositories\WarehouseRepository;
use WMS\Repositories\BatchRepository;

/**
 * Servicio de sincronización SAP B1 <-> WMS.
 *
 * Estrategia:
 * - Maestros (items, warehouses, BP): sync periódica SAP -> WMS
 * - Documentos transaccionales: sync por evento o periódica
 * - Confirmaciones: WMS -> SAP al completar operaciones
 * - Errores y reintentos: log en sap_sync_log con backoff
 */
class SapSyncService
{
    private SapServiceLayerClient $sap;
    private ItemRepository $itemRepo;
    private WarehouseRepository $warehouseRepo;
    private BatchRepository $batchRepo;

    public function __construct()
    {
        $this->sap           = new SapServiceLayerClient();
        $this->itemRepo      = new ItemRepository();
        $this->warehouseRepo = new WarehouseRepository();
        $this->batchRepo     = new BatchRepository();
    }

    // ── SINCRONIZACIÓN DE MAESTROS ──────────────────────────────

    /**
     * Sincroniza artículos desde SAP al WMS.
     */
    public function syncItems(): array
    {
        $stats = ['created' => 0, 'updated' => 0, 'errors' => 0];
        $skip = 0;
        $top = 100;

        do {
            try {
                $sapItems = $this->sap->getItems($top, $skip);
                $items = $sapItems['value'] ?? [];

                foreach ($items as $sapItem) {
                    try {
                        $existing = $this->itemRepo->findByCode($sapItem['ItemCode']);

                        $data = [
                            'item_code'       => $sapItem['ItemCode'],
                            'item_name'       => $sapItem['ItemName'],
                            'barcode'         => $sapItem['BarCode'] ?? null,
                            'uom'             => $sapItem['InventoryUOM'] ?? 'UN',
                            'requires_lot'    => ($sapItem['ManageBatchNumbers'] ?? 'tNO') === 'tYES' ? 1 : 0,
                            'requires_serial' => ($sapItem['ManageSerialNumbers'] ?? 'tNO') === 'tYES' ? 1 : 0,
                            'requires_expiry' => 1, // Farmacéutica: siempre
                            'sap_item_code'   => $sapItem['ItemCode'],
                        ];

                        if ($existing) {
                            $this->itemRepo->update($existing['id'], $data);
                            $stats['updated']++;
                        } else {
                            $this->itemRepo->insert($data);
                            $stats['created']++;
                        }
                    } catch (\Throwable $e) {
                        $stats['errors']++;
                        $this->logSync('MASTER_ITEMS', 'SAP_TO_WMS', null, null, null, 'ERROR', $e->getMessage());
                    }
                }

                $skip += $top;
            } catch (\Throwable $e) {
                $this->logSync('MASTER_ITEMS', 'SAP_TO_WMS', null, null, null, 'ERROR', $e->getMessage());
                break;
            }
        } while (count($items ?? []) === $top);

        $this->logSync('MASTER_ITEMS', 'SAP_TO_WMS', null, null, null, 'SUCCESS', json_encode($stats));
        return $stats;
    }

    /**
     * Sincroniza almacenes desde SAP al WMS.
     */
    public function syncWarehouses(): array
    {
        $stats = ['created' => 0, 'updated' => 0];

        $sapWarehouses = $this->sap->getWarehouses();
        foreach ($sapWarehouses['value'] ?? [] as $sapWh) {
            $existing = $this->warehouseRepo->findByCode($sapWh['WarehouseCode']);
            $data = [
                'code'               => $sapWh['WarehouseCode'],
                'name'               => $sapWh['WarehouseName'],
                'address'            => trim(($sapWh['Street'] ?? '') . ' ' . ($sapWh['City'] ?? '')),
                'sap_warehouse_code' => $sapWh['WarehouseCode'],
            ];

            if ($existing) {
                $this->warehouseRepo->update($existing['id'], $data);
                $stats['updated']++;
            } else {
                $this->warehouseRepo->insert($data);
                $stats['created']++;
            }
        }

        $this->logSync('MASTER_WAREHOUSES', 'SAP_TO_WMS', null, null, null, 'SUCCESS', json_encode($stats));
        return $stats;
    }

    // ── CONFIRMACIÓN A SAP ──────────────────────────────────────

    /**
     * Confirma una recepción en SAP: crea Goods Receipt PO.
     */
    public function confirmReceiptToSap(int $docId): array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare(
            "SELECT d.*, dl.item_id, dl.received_qty, dl.batch_number, dl.expiry_date,
                    dl.sap_line_num, i.sap_item_code
             FROM inventory_documents d
             JOIN inventory_document_lines dl ON d.id = dl.document_id
             JOIN items i ON dl.item_id = i.id
             WHERE d.id = ? AND dl.status IN ('PARTIAL','COMPLETED')"
        );
        $stmt->execute([$docId]);
        $rows = $stmt->fetchAll();

        if (empty($rows)) {
            throw new \RuntimeException('Sin líneas confirmadas para enviar a SAP');
        }

        $header = $rows[0];
        $sapData = [
            'CardCode'      => $header['bp_code'],
            'DocDate'       => date('Y-m-d'),
            'DocumentLines' => [],
        ];

        foreach ($rows as $row) {
            $line = [
                'ItemCode'      => $row['sap_item_code'],
                'Quantity'      => (float) $row['received_qty'],
                'WarehouseCode' => $header['sap_doc_type'] ?? null,
            ];

            // Agregar lote si aplica
            if ($row['batch_number']) {
                $line['BatchNumbers'] = [[
                    'BatchNumber' => $row['batch_number'],
                    'Quantity'    => (float) $row['received_qty'],
                    'ExpiryDate'  => $row['expiry_date'],
                ]];
            }

            // Enlazar con orden de compra original
            if ($header['sap_doc_entry']) {
                $line['BaseEntry'] = $header['sap_doc_entry'];
                $line['BaseLine']  = $row['sap_line_num'];
                $line['BaseType']  = 22; // PurchaseOrder
            }

            $sapData['DocumentLines'][] = $line;
        }

        try {
            $result = $this->sap->createGoodsReceiptPO($sapData);
            $this->logSync('DOC_RECEIPT', 'WMS_TO_SAP', $result['DocEntry'] ?? null, 'GoodsReceiptPO', $docId, 'SUCCESS');

            // Actualizar referencia SAP en el documento WMS
            if (isset($result['DocEntry'])) {
                $db->prepare(
                    "UPDATE inventory_documents SET sap_doc_entry = ?, sap_doc_num = ? WHERE id = ?"
                )->execute([$result['DocEntry'], $result['DocNum'] ?? null, $docId]);
            }

            return $result;
        } catch (\Throwable $e) {
            $this->logSync('DOC_RECEIPT', 'WMS_TO_SAP', null, 'GoodsReceiptPO', $docId, 'ERROR', $e->getMessage());
            throw $e;
        }
    }

    /**
     * Confirma un despacho en SAP: crea Delivery Note.
     */
    public function confirmDeliveryToSap(int $pickListId): array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare(
            "SELECT pl.*, pll.item_id, pll.picked_qty, pll.batch_id,
                    pll.sap_line_num, i.sap_item_code, bt.batch_number
             FROM pick_lists pl
             JOIN pick_list_lines pll ON pl.id = pll.pick_list_id
             JOIN items i ON pll.item_id = i.id
             LEFT JOIN batches bt ON pll.batch_id = bt.id
             WHERE pl.id = ? AND pll.status = 'PICKED'"
        );
        $stmt->execute([$pickListId]);
        $rows = $stmt->fetchAll();

        if (empty($rows)) {
            throw new \RuntimeException('Sin líneas pickeadas para enviar a SAP');
        }

        $header = $rows[0];
        $sapData = [
            'CardCode'      => $header['customer_code'],
            'DocDate'       => date('Y-m-d'),
            'DocumentLines' => [],
        ];

        foreach ($rows as $row) {
            $line = [
                'ItemCode' => $row['sap_item_code'],
                'Quantity' => (float) $row['picked_qty'],
            ];

            if ($row['batch_number']) {
                $line['BatchNumbers'] = [[
                    'BatchNumber' => $row['batch_number'],
                    'Quantity'    => (float) $row['picked_qty'],
                ]];
            }

            if ($header['sap_doc_entry']) {
                $line['BaseEntry'] = $header['sap_doc_entry'];
                $line['BaseLine']  = $row['sap_line_num'];
                $line['BaseType']  = 17; // SalesOrder
            }

            $sapData['DocumentLines'][] = $line;
        }

        try {
            $result = $this->sap->createDelivery($sapData);
            $this->logSync('DOC_DELIVERY', 'WMS_TO_SAP', $result['DocEntry'] ?? null, 'DeliveryNote', $pickListId, 'SUCCESS');
            return $result;
        } catch (\Throwable $e) {
            $this->logSync('DOC_DELIVERY', 'WMS_TO_SAP', null, 'DeliveryNote', $pickListId, 'ERROR', $e->getMessage());
            throw $e;
        }
    }

    /**
     * Confirma empaque en SAP: crea DeliveryNote desde la orden de packing.
     */
    public function confirmPackingToSap(int $packingId): array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare(
            "SELECT po.*, pol.item_id, pol.packed_qty, pol.batch_id,
                    pll.sap_line_num, i.sap_item_code, bt.batch_number
             FROM packing_orders po
             JOIN packing_lines pol ON po.id = pol.packing_order_id
             JOIN items i ON pol.item_id = i.id
             LEFT JOIN batches bt ON pol.batch_id = bt.id
             LEFT JOIN pick_list_lines pll
                ON pll.pick_list_id = po.pick_list_id
               AND pll.item_id = pol.item_id
               AND (pll.batch_id <=> pol.batch_id)
             WHERE po.id = ? AND pol.status = 'PACKED'"
        );
        $stmt->execute([$packingId]);
        $rows = $stmt->fetchAll();

        if (empty($rows)) {
            throw new \RuntimeException('Sin líneas empacadas para enviar a SAP');
        }

        $header = $rows[0];
        $sapData = [
            'CardCode'      => $header['customer_code'],
            'DocDate'       => date('Y-m-d'),
            'DocumentLines' => [],
        ];

        foreach ($rows as $row) {
            $line = [
                'ItemCode' => $row['sap_item_code'],
                'Quantity' => (float) $row['packed_qty'],
            ];

            if ($row['batch_number']) {
                $line['BatchNumbers'] = [[
                    'BatchNumber' => $row['batch_number'],
                    'Quantity'    => (float) $row['packed_qty'],
                ]];
            }

            if ($header['sap_doc_entry']) {
                $line['BaseEntry'] = (int) $header['sap_doc_entry'];
                $line['BaseLine']  = (int) ($row['sap_line_num'] ?? 0);
                $line['BaseType']  = 17; // SalesOrder
            }

            $sapData['DocumentLines'][] = $line;
        }

        try {
            $result = $this->sap->createDelivery($sapData);
            $this->logSync('DOC_PACKING', 'WMS_TO_SAP', $result['DocEntry'] ?? null, 'DeliveryNote', $packingId, 'SUCCESS');

            if (isset($result['DocEntry'])) {
                $db->prepare(
                    "UPDATE packing_orders SET sap_delivery_entry = ?, sap_delivery_num = ? WHERE id = ?"
                )->execute([$result['DocEntry'], $result['DocNum'] ?? null, $packingId]);
            }

            return $result;
        } catch (\Throwable $e) {
            $this->logSync('DOC_PACKING', 'WMS_TO_SAP', null, 'DeliveryNote', $packingId, 'ERROR', $e->getMessage());
            throw $e;
        }
    }

    /**
     * Confirma despacho en SAP: crea DeliveryNotes pendientes del despacho.
     */
    public function confirmDispatchToSap(int $dispatchId): array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare(
            "SELECT dl.packing_order_id
             FROM dispatch_lines dl
             JOIN packing_orders po ON dl.packing_order_id = po.id
             WHERE dl.dispatch_id = ? AND po.sap_delivery_entry IS NULL"
        );
        $stmt->execute([$dispatchId]);
        $pendingPackings = $stmt->fetchAll();

        $results = [];
        foreach ($pendingPackings as $row) {
            try {
                $results[] = $this->confirmPackingToSap($row['packing_order_id']);
            } catch (\Throwable $e) {
                $this->logSync('DOC_DISPATCH', 'WMS_TO_SAP', null, 'DeliveryNote', $dispatchId, 'ERROR', $e->getMessage());
            }
        }

        $this->logSync('DOC_DISPATCH', 'WMS_TO_SAP', null, 'DeliveryNote', $dispatchId, 'SUCCESS', json_encode(['deliveries_created' => count($results)]));
        return $results;
    }

    /**
     * Confirma devolución en SAP: crea CreditNote.
     */
    public function confirmReturnToSap(int $docId): array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare(
            "SELECT d.*, dl.item_id, dl.received_qty, dl.batch_number, dl.expiry_date,
                    dl.sap_line_num, i.sap_item_code
             FROM inventory_documents d
             JOIN inventory_document_lines dl ON d.id = dl.document_id
             JOIN items i ON dl.item_id = i.id
             WHERE d.id = ? AND d.doc_type = 'RETURN' AND dl.status IN ('PARTIAL','COMPLETED')"
        );
        $stmt->execute([$docId]);
        $rows = $stmt->fetchAll();

        if (empty($rows)) {
            throw new \RuntimeException('Sin líneas de devolución confirmadas para enviar a SAP');
        }

        $header = $rows[0];
        $sapData = [
            'CardCode'      => $header['bp_code'],
            'DocDate'       => date('Y-m-d'),
            'DocumentLines' => [],
        ];

        foreach ($rows as $row) {
            $line = [
                'ItemCode' => $row['sap_item_code'],
                'Quantity' => (float) $row['received_qty'],
            ];

            if ($header['sap_doc_entry']) {
                $line['BaseEntry'] = (int) $header['sap_doc_entry'];
                $line['BaseLine']  = (int) ($row['sap_line_num'] ?? 0);
                $line['BaseType']  = 14; // DeliveryNote
            }

            $sapData['DocumentLines'][] = $line;
        }

        try {
            $result = $this->sap->createCreditNote($sapData);
            $this->logSync('DOC_RETURN', 'WMS_TO_SAP', $result['DocEntry'] ?? null, 'CreditNote', $docId, 'SUCCESS');

            if (isset($result['DocEntry'])) {
                $db->prepare(
                    "UPDATE inventory_documents SET sap_doc_entry = ?, sap_doc_num = ? WHERE id = ?"
                )->execute([$result['DocEntry'], $result['DocNum'] ?? null, $docId]);
            }

            return $result;
        } catch (\Throwable $e) {
            $this->logSync('DOC_RETURN', 'WMS_TO_SAP', null, 'CreditNote', $docId, 'ERROR', $e->getMessage());
            throw $e;
        }
    }

    // ── REINTENTOS ──────────────────────────────────────────────

    /**
     * Procesa reintentos pendientes del log de sync.
     */
    public function processRetries(): array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare(
            "SELECT * FROM sap_sync_log
             WHERE status IN ('ERROR','RETRY')
               AND retry_count < max_retries
               AND (next_retry_at IS NULL OR next_retry_at <= NOW())
             ORDER BY created_at ASC LIMIT 10"
        );
        $stmt->execute();
        $pending = $stmt->fetchAll();

        $results = ['processed' => 0, 'success' => 0, 'failed' => 0];

        foreach ($pending as $entry) {
            $results['processed']++;
            try {
                // Reintentar según tipo
                if ($entry['direction'] === 'WMS_TO_SAP' && $entry['wms_reference_id']) {
                    if ($entry['sync_type'] === 'DOC_RECEIPT') {
                        $this->confirmReceiptToSap($entry['wms_reference_id']);
                    } elseif ($entry['sync_type'] === 'DOC_DELIVERY') {
                        $this->confirmDeliveryToSap($entry['wms_reference_id']);
                    } elseif ($entry['sync_type'] === 'DOC_PACKING') {
                        $this->confirmPackingToSap($entry['wms_reference_id']);
                    } elseif ($entry['sync_type'] === 'DOC_DISPATCH') {
                        $this->confirmDispatchToSap($entry['wms_reference_id']);
                    } elseif ($entry['sync_type'] === 'DOC_RETURN') {
                        $this->confirmReturnToSap($entry['wms_reference_id']);
                    }
                }
                $results['success']++;
            } catch (\Throwable $e) {
                $retryCount = $entry['retry_count'] + 1;
                $nextRetry = date('Y-m-d H:i:s', strtotime("+{$retryCount} minutes"));
                $newStatus = $retryCount >= $entry['max_retries'] ? 'ERROR' : 'RETRY';

                $db->prepare(
                    "UPDATE sap_sync_log SET status = ?, retry_count = ?, next_retry_at = ?,
                     error_message = ? WHERE id = ?"
                )->execute([$newStatus, $retryCount, $nextRetry, $e->getMessage(), $entry['id']]);

                $results['failed']++;
            }
        }

        return $results;
    }

    // ── HELPERS ─────────────────────────────────────────────────

    private function logSync(
        string $syncType,
        string $direction,
        ?int $sapDocEntry,
        ?string $sapDocType = null,
        ?int $wmsRefId = null,
        string $status = 'SUCCESS',
        ?string $errorMessage = null
    ): void {
        $db = Database::getConnection();
        $db->prepare(
            "INSERT INTO sap_sync_log
             (sync_type, direction, sap_doc_entry, sap_doc_type, wms_reference_id, status, error_message)
             VALUES (?, ?, ?, ?, ?, ?, ?)"
        )->execute([$syncType, $direction, $sapDocEntry, $sapDocType, $wmsRefId, $status, $errorMessage]);
    }
}
