<?php

namespace WMS\Services;

use WMS\Core\Database;
use WMS\Repositories\DispatchRepository;
use WMS\Repositories\PackingRepository;
use WMS\Repositories\ItemRepository;
use WMS\Repositories\StockRepository;
use WMS\Repositories\StockMovementRepository;
use WMS\Integrations\Sap\SapServiceLayerClient;

/**
 * Servicio de Despacho / Expedición.
 *
 * Flujo:
 * 1. Se crea un despacho (ruta, transportista, fecha)
 * 2. Se asignan órdenes de empaque completadas al despacho
 * 3. Se escanean contenedores para verificación de carga
 * 4. Al cerrar se crean DeliveryNotes en SAP si no fueron creados en packing,
 *    y se emite el stock (salida de almacén)
 */
class DispatchService
{
    private DispatchRepository $dispatchRepo;
    private PackingRepository $packingRepo;
    private ItemRepository $itemRepo;
    private StockRepository $stockRepo;
    private StockMovementRepository $movementRepo;

    public function __construct()
    {
        $this->dispatchRepo = new DispatchRepository();
        $this->packingRepo  = new PackingRepository();
        $this->itemRepo     = new ItemRepository();
        $this->stockRepo    = new StockRepository();
        $this->movementRepo = new StockMovementRepository();
    }

    /**
     * Crea un nuevo despacho.
     */
    public function createDispatch(array $data, int $userId): array
    {
        $dispatchNumber = Database::nextDocNumber('DISP');

        $dispatchId = $this->dispatchRepo->insert([
            'dispatch_number' => $dispatchNumber,
            'warehouse_id'    => (int) $data['warehouse_id'],
            'status'          => 'OPEN',
            'carrier'         => $data['carrier'] ?? null,
            'vehicle_plate'   => $data['vehicle_plate'] ?? null,
            'driver_name'     => $data['driver_name'] ?? null,
            'route'           => $data['route'] ?? null,
            'scheduled_date'  => $data['scheduled_date'] ?? null,
            'notes'           => $data['notes'] ?? null,
            'created_by'      => $userId,
        ]);

        return $this->dispatchRepo->getWithLines($dispatchId);
    }

    /**
     * Asigna una orden de empaque completada al despacho.
     */
    public function assignOrder(int $dispatchId, array $data, int $userId): array
    {
        $dispatch = $this->dispatchRepo->findById($dispatchId);
        if (!$dispatch || $dispatch['status'] === 'CLOSED' || $dispatch['status'] === 'CANCELLED') {
            throw new \RuntimeException('Despacho no válido para asignación');
        }

        $packingId = (int) $data['packing_order_id'];
        $packing = $this->packingRepo->findById($packingId);

        if (!$packing) {
            throw new \RuntimeException('Orden de empaque no encontrada');
        }
        if ($packing['status'] !== 'COMPLETED') {
            throw new \RuntimeException('La orden de empaque debe estar completada');
        }

        $db = Database::getConnection();

        // Verificar que no esté ya asignada a otro despacho
        $stmt = $db->prepare(
            "SELECT dispatch_id FROM dispatch_lines WHERE packing_order_id = ? LIMIT 1"
        );
        $stmt->execute([$packingId]);
        $existing = $stmt->fetch();
        if ($existing) {
            throw new \RuntimeException('Esta orden de empaque ya está asignada a un despacho');
        }

        // Obtener próximo line_num
        $stmt = $db->prepare(
            "SELECT COALESCE(MAX(line_num), 0) + 1 FROM dispatch_lines WHERE dispatch_id = ?"
        );
        $stmt->execute([$dispatchId]);
        $nextLine = (int) $stmt->fetchColumn();

        $db->prepare(
            "INSERT INTO dispatch_lines
             (dispatch_id, line_num, packing_order_id, customer_code, customer_name, status)
             VALUES (?, ?, ?, ?, ?, 'PENDING')"
        )->execute([
            $dispatchId,
            $nextLine,
            $packingId,
            $packing['customer_code'] ?? null,
            $packing['customer_name'] ?? null,
        ]);

        // Actualizar estado del despacho
        $this->dispatchRepo->update($dispatchId, ['status' => 'LOADING']);

        return $this->dispatchRepo->getWithLines($dispatchId);
    }

    /**
     * Escanea un contenedor para verificación de carga.
     */
    public function scanContainer(int $dispatchId, array $data, int $userId): array
    {
        $dispatch = $this->dispatchRepo->findById($dispatchId);
        if (!$dispatch || !in_array($dispatch['status'], ['OPEN', 'LOADING'])) {
            throw new \RuntimeException('Despacho no válido para escaneo');
        }

        $containerCode = $data['container_code'] ?? null;
        if (!$containerCode) {
            throw new \RuntimeException('Código de contenedor requerido');
        }

        $db = Database::getConnection();

        // Buscar el contenedor en las líneas de empaque asignadas al despacho
        $stmt = $db->prepare(
            "SELECT dl.id AS dispatch_line_id, dl.packing_order_id, pol.container_code
             FROM dispatch_lines dl
             JOIN packing_order_lines pol ON pol.packing_order_id = dl.packing_order_id
             WHERE dl.dispatch_id = ? AND pol.container_code = ?
             LIMIT 1"
        );
        $stmt->execute([$dispatchId, $containerCode]);
        $match = $stmt->fetch();

        if (!$match) {
            throw new \RuntimeException("Contenedor '{$containerCode}' no pertenece a este despacho");
        }

        // Marcar la línea del despacho como verificada
        $db->prepare(
            "UPDATE dispatch_lines SET status = 'VERIFIED', verified_at = NOW(), verified_by = ?
             WHERE id = ?"
        )->execute([$userId, $match['dispatch_line_id']]);

        return $this->dispatchRepo->getWithLines($dispatchId);
    }

    /**
     * Genera un packing list del despacho con items agrupados por cliente.
     * Devuelve un JSON listo para imprimir.
     */
    public function generatePackingList(int $dispatchId): array
    {
        $dispatch = $this->dispatchRepo->getWithLines($dispatchId);
        if (!$dispatch) {
            throw new \RuntimeException('Despacho no encontrado');
        }

        $db = Database::getConnection();
        $byCustomer = [];

        foreach ($dispatch['lines'] ?? [] as $line) {
            $packingId = $line['packing_order_id'] ?? null;
            if (!$packingId) continue;

            $packing = $this->packingRepo->getWithLines($packingId);
            if (!$packing) continue;

            $customerKey = $packing['customer_code'] ?? $packing['customer_name'] ?? 'SIN_CLIENTE';
            $customerName = $packing['customer_name'] ?? $customerKey;

            if (!isset($byCustomer[$customerKey])) {
                $byCustomer[$customerKey] = [
                    'customer_code' => $packing['customer_code'] ?? null,
                    'customer_name' => $customerName,
                    'packing_orders' => [],
                    'items' => [],
                    'total_items' => 0,
                ];
            }

            $byCustomer[$customerKey]['packing_orders'][] = $packing['pack_number'] ?? $packingId;

            foreach ($packing['lines'] ?? [] as $packLine) {
                $item = $this->itemRepo->findById($packLine['item_id']);
                $byCustomer[$customerKey]['items'][] = [
                    'item_code'      => $item['item_code'] ?? $item['sap_item_code'] ?? null,
                    'item_name'      => $item['item_name'] ?? $item['name'] ?? null,
                    'batch_number'   => $packLine['batch_number'] ?? null,
                    'quantity'       => (float) ($packLine['packed_qty'] ?? $packLine['expected_qty'] ?? 0),
                    'uom'            => $packLine['uom'] ?? 'UN',
                    'container_code' => $packLine['container_code'] ?? null,
                    'pack_number'    => $packing['pack_number'] ?? null,
                ];
                $byCustomer[$customerKey]['total_items']++;
            }
        }

        return [
            'dispatch_number' => $dispatch['dispatch_number'] ?? null,
            'dispatch_id'     => $dispatchId,
            'truck_plate'     => $dispatch['truck_plate'] ?? $dispatch['vehicle_plate'] ?? null,
            'driver_name'     => $dispatch['driver_name'] ?? null,
            'carrier'         => $dispatch['carrier'] ?? null,
            'status'          => $dispatch['status'],
            'date'            => $dispatch['created_at'] ?? date('Y-m-d H:i:s'),
            'customers'       => array_values($byCustomer),
            'total_customers' => count($byCustomer),
        ];
    }

    /**
     * Cierra el despacho.
     * Crea DeliveryNotes en SAP si no fueron creados en packing.
     * Emite stock (salida de almacén).
     */
    public function closeDispatch(int $dispatchId, int $userId): array
    {
        $dispatch = $this->dispatchRepo->getWithLines($dispatchId);
        if (!$dispatch || $dispatch['status'] === 'CLOSED' || $dispatch['status'] === 'CANCELLED') {
            throw new \RuntimeException('Despacho no válido para cierre');
        }

        $db = Database::getConnection();
        $db->beginTransaction();

        try {
            // Emitir stock para cada orden de empaque
            foreach ($dispatch['lines'] as $line) {
                $packingId = $line['packing_order_id'];
                $packing = $this->packingRepo->getWithLines($packingId);
                if (!$packing) continue;

                foreach ($packing['lines'] as $packLine) {
                    // Registrar movimiento de salida
                    $this->movementRepo->logMovement([
                        'warehouse_id'   => $dispatch['warehouse_id'],
                        'movement_type'  => 'DISPATCH',
                        'item_id'        => $packLine['item_id'],
                        'batch_id'       => $packLine['batch_id'] ?? null,
                        'from_status'    => 'AVAILABLE',
                        'quantity'       => (float) $packLine['packed_qty'],
                        'uom'            => $packLine['uom'] ?? 'UN',
                        'reference_type' => 'DISPATCH',
                        'reference_id'   => $dispatchId,
                        'created_by'     => $userId,
                    ]);
                }

                // Crear DeliveryNote en SAP si no fue creado en packing
                if (empty($packing['sap_delivery_entry']) && !empty($packing['sap_doc_entry'])) {
                    try {
                        $sapData = [
                            'CardCode'      => $packing['customer_code'],
                            'DocDate'       => date('Y-m-d'),
                            'DocumentLines' => [],
                        ];

                        foreach ($packing['lines'] as $packLine) {
                            $item = $this->itemRepo->findById($packLine['item_id']);
                            $sapLine = [
                                'ItemCode'  => $item['sap_item_code'],
                                'Quantity'  => (float) $packLine['packed_qty'],
                                'BaseType'  => 17, // SalesOrder
                                'BaseEntry' => (int) $packing['sap_doc_entry'],
                                'BaseLine'  => (int) ($packLine['sap_line_num'] ?? 0),
                            ];

                            if (!empty($packLine['batch_number'])) {
                                $sapLine['BatchNumbers'] = [[
                                    'BatchNumber' => $packLine['batch_number'],
                                    'Quantity'    => (float) $packLine['packed_qty'],
                                ]];
                            }

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

                        $sap = new SapServiceLayerClient();
                        $result = $sap->post('/DeliveryNotes', $sapData);

                        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]);
                        }

                        $db->prepare(
                            "INSERT INTO sap_sync_log
                             (sync_type, direction, sap_doc_entry, sap_doc_type, wms_reference_id, status)
                             VALUES (?, ?, ?, ?, ?, ?)"
                        )->execute(['DOC_DISPATCH', 'WMS_TO_SAP', $result['DocEntry'] ?? null, 'DeliveryNote', $dispatchId, 'SUCCESS']);
                    } catch (\Throwable $e) {
                        $db->prepare(
                            "INSERT INTO sap_sync_log
                             (sync_type, direction, sap_doc_type, wms_reference_id, status, error_message)
                             VALUES (?, ?, ?, ?, ?, ?)"
                        )->execute(['DOC_DISPATCH', 'WMS_TO_SAP', 'DeliveryNote', $dispatchId, 'ERROR', $e->getMessage()]);
                    }
                }
            }

            $this->dispatchRepo->update($dispatchId, [
                'status'    => 'CLOSED',
                'closed_at' => date('Y-m-d H:i:s'),
                'closed_by' => $userId,
            ]);

            $db->commit();

            return $this->dispatchRepo->getWithLines($dispatchId);
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }
}
