<?php

namespace WMS\Services;

use WMS\Core\Database;
use WMS\Repositories\StockRepository;
use WMS\Repositories\StockMovementRepository;

/**
 * Reubicación de LPN (container) entre bins de la MISMA zona.
 * Crea la reubicación + una tarea PICK/MOVE en warehouse_tasks.
 * El operador completa el movimiento físico en RF.
 */
class RelocationService
{
    private StockRepository $stockRepo;
    private StockMovementRepository $movementRepo;

    public function __construct()
    {
        $this->stockRepo    = new StockRepository();
        $this->movementRepo = new StockMovementRepository();
    }

    /**
     * Crea reubicación: valida misma zona, crea tarea, asigna usuario.
     */
    public function create(array $data, int $userId): array
    {
        $containerId = (int) ($data['container_id'] ?? 0);
        $toBinId     = (int) ($data['to_bin_id'] ?? 0);
        $assignedTo  = isset($data['assigned_to']) && $data['assigned_to'] !== ''
                        ? (int) $data['assigned_to'] : null;
        $notes       = trim((string) ($data['notes'] ?? '')) ?: null;

        if ($containerId <= 0) throw new \RuntimeException('container_id es obligatorio');
        if ($toBinId <= 0)     throw new \RuntimeException('to_bin_id es obligatorio');

        $db = Database::getConnection();

        // 1. Validar container existe
        $stmt = $db->prepare("SELECT id, container_code, warehouse_id, status FROM containers WHERE id = ?");
        $stmt->execute([$containerId]);
        $container = $stmt->fetch();
        if (!$container) throw new \RuntimeException('LPN no encontrada');
        if (in_array($container['status'], ['SHIPPED','PUTAWAY','CANCELLED'], true)) {
            throw new \RuntimeException('LPN en estado ' . $container['status'] . ' — no se puede reubicar');
        }

        // 2. Derivar bin origen a partir del stock de los items de la LPN.
        //    Asume LPN homogéneo (todos los items en mismo bin).
        $stmt = $db->prepare(
            "SELECT DISTINCT s.bin_id, b.zone_id, b.code AS bin_code
             FROM container_lines cl
             JOIN stock s ON s.item_id = cl.item_id
                          AND (s.batch_id <=> cl.batch_id)
                          AND s.warehouse_id = ?
                          AND s.quantity > 0
             JOIN bins b ON b.id = s.bin_id
             WHERE cl.container_id = ?
             LIMIT 5"
        );
        $stmt->execute([$container['warehouse_id'], $containerId]);
        $origins = $stmt->fetchAll();

        if (empty($origins)) {
            throw new \RuntimeException('La LPN no tiene stock asociado en ningún bin');
        }
        if (count($origins) > 1) {
            throw new \RuntimeException('La LPN está distribuida en múltiples bins; reubicación por LPN requiere un solo bin origen');
        }
        $fromBinId = (int) $origins[0]['bin_id'];
        $fromZoneId = (int) $origins[0]['zone_id'];

        // 3. Validar bin destino + misma zona
        $stmt = $db->prepare("SELECT id, code, zone_id, warehouse_id, is_active, bin_type FROM bins WHERE id = ?");
        $stmt->execute([$toBinId]);
        $toBin = $stmt->fetch();
        if (!$toBin) throw new \RuntimeException('Bin destino no encontrado');
        if (!$toBin['is_active']) throw new \RuntimeException('Bin destino inactivo');
        if ((int)$toBin['warehouse_id'] !== (int)$container['warehouse_id']) {
            throw new \RuntimeException('Bin destino pertenece a otro almacén');
        }
        if ($toBinId === $fromBinId) {
            throw new \RuntimeException('Bin origen y destino son el mismo');
        }
        if ((int)$toBin['zone_id'] !== $fromZoneId) {
            throw new \RuntimeException('Bin destino está en otra zona. Reubicación solo permitida dentro de la misma zona.');
        }

        // 4. Generar número, insertar
        $relocNumber = Database::nextDocNumber('RELOC');

        $db->beginTransaction();
        try {
            $stmt = $db->prepare(
                "INSERT INTO relocations
                 (relocation_number, warehouse_id, container_id, from_bin_id, to_bin_id, zone_id,
                  status, assigned_to, notes, created_by)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
            );
            $status = $assignedTo ? 'ASSIGNED' : 'PENDING';
            $stmt->execute([
                $relocNumber, $container['warehouse_id'], $containerId,
                $fromBinId, $toBinId, $fromZoneId,
                $status, $assignedTo, $notes, $userId,
            ]);
            $relocId = (int) $db->lastInsertId();

            // 5. Crear tarea MOVE en warehouse_tasks
            $stmt = $db->prepare(
                "INSERT INTO warehouse_tasks
                 (warehouse_id, task_type, priority, status, item_id, from_bin_id, to_bin_id,
                  quantity, uom, assigned_to, reference_type, reference_id, notes, created_by)
                 SELECT ?, 'MOVE', 3, ?, MIN(cl.item_id), ?, ?, 1, 'LPN', ?, 'RELOCATION', ?, ?, ?
                 FROM container_lines cl WHERE cl.container_id = ?"
            );
            $stmt->execute([
                $container['warehouse_id'],
                $assignedTo ? 'ASSIGNED' : 'PENDING',
                $fromBinId, $toBinId,
                $assignedTo,
                $relocId,
                'Reubicación ' . $relocNumber . ' — LPN ' . $container['container_code'],
                $userId,
                $containerId,
            ]);
            $taskId = (int) $db->lastInsertId();

            $db->prepare("UPDATE relocations SET task_id = ? WHERE id = ?")->execute([$taskId, $relocId]);

            $db->commit();

            \WMS\Services\AuditService::log('relocations', $relocId, 'RELOCATION_CREATE', null, [
                'relocation_number' => $relocNumber,
                'container_id'      => $containerId,
                'from_bin_id'       => $fromBinId,
                'to_bin_id'         => $toBinId,
                'zone_id'           => $fromZoneId,
                'assigned_to'       => $assignedTo,
                'task_id'           => $taskId,
            ], $userId);

            return $this->findById($relocId);
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }

    /**
     * Devuelve contexto necesario para mostrar el modal "Nueva Reubicación":
     * - container info
     * - from_bin (derivado del stock)
     * - zone (from_bin.zone_id)
     * - available_to_bins (mismos almacen + zona, activos, STORAGE, distinto del origen)
     */
    public function lookup(int $containerId): array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare("SELECT id, container_code, container_type, warehouse_id, status FROM containers WHERE id = ?");
        $stmt->execute([$containerId]);
        $container = $stmt->fetch();
        if (!$container) throw new \RuntimeException('LPN no encontrada');

        $stmt = $db->prepare(
            "SELECT DISTINCT s.bin_id, b.code AS bin_code, b.zone_id,
                    z.code AS zone_code, z.name AS zone_name
             FROM container_lines cl
             JOIN stock s ON s.item_id = cl.item_id
                          AND (s.batch_id <=> cl.batch_id)
                          AND s.warehouse_id = ?
                          AND s.quantity > 0
             JOIN bins b ON b.id = s.bin_id
             LEFT JOIN warehouse_zones z ON z.id = b.zone_id
             WHERE cl.container_id = ?
             LIMIT 5"
        );
        $stmt->execute([$container['warehouse_id'], $containerId]);
        $origins = $stmt->fetchAll();

        $result = [
            'container'         => $container,
            'from_bin_id'       => null,
            'from_bin_code'     => null,
            'zone_id'           => null,
            'zone_code'         => null,
            'zone_name'         => null,
            'multi_bin'         => count($origins) > 1,
            'no_stock'          => empty($origins),
            'available_to_bins' => [],
        ];

        if (empty($origins)) return $result;
        if (count($origins) > 1) return $result;

        $o = $origins[0];
        $result['from_bin_id']   = (int) $o['bin_id'];
        $result['from_bin_code'] = $o['bin_code'];
        $result['zone_id']       = $o['zone_id'] !== null ? (int)$o['zone_id'] : null;
        $result['zone_code']     = $o['zone_code'];
        $result['zone_name']     = $o['zone_name'];

        if ($result['zone_id'] !== null) {
            $stmt = $db->prepare(
                "SELECT id, code FROM bins
                 WHERE warehouse_id = ? AND zone_id = ? AND id <> ?
                   AND is_active = 1 AND bin_type = 'STORAGE'
                 ORDER BY code"
            );
            $stmt->execute([$container['warehouse_id'], $result['zone_id'], $result['from_bin_id']]);
            $result['available_to_bins'] = $stmt->fetchAll();
        }

        return $result;
    }

    public function findById(int $id): ?array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare(
            "SELECT r.*, c.container_code, c.container_type,
                    bf.code AS from_bin_code, bt.code AS to_bin_code,
                    z.code AS zone_code, z.name AS zone_name,
                    u.full_name AS assigned_to_name
             FROM relocations r
             JOIN containers c ON c.id = r.container_id
             JOIN bins bf ON bf.id = r.from_bin_id
             JOIN bins bt ON bt.id = r.to_bin_id
             JOIN warehouse_zones z ON z.id = r.zone_id
             LEFT JOIN users u ON u.id = r.assigned_to
             WHERE r.id = ?"
        );
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        return $row ?: null;
    }

    /**
     * Devuelve reubicaciones del almacén ordenadas por id DESC.
     * Por defecto solo activas (PENDING/ASSIGNED/IN_PROGRESS). Con $includeAll=true
     * devuelve también COMPLETED/CANCELLED para uso en panel supervisor.
     */
    public function listOpen(int $warehouseId, bool $includeAll = false, int $limit = 200): array
    {
        $db = Database::getConnection();
        $where = "r.warehouse_id = ?";
        $params = [$warehouseId];
        if (!$includeAll) {
            $where .= " AND r.status IN ('PENDING','ASSIGNED','IN_PROGRESS')";
        }
        $stmt = $db->prepare(
            "SELECT r.*, c.container_code,
                    bf.code AS from_bin_code, bt.code AS to_bin_code,
                    z.code AS zone_code, z.name AS zone_name,
                    u.full_name AS assigned_to_name
             FROM relocations r
             JOIN containers c ON c.id = r.container_id
             JOIN bins bf ON bf.id = r.from_bin_id
             JOIN bins bt ON bt.id = r.to_bin_id
             JOIN warehouse_zones z ON z.id = r.zone_id
             LEFT JOIN users u ON u.id = r.assigned_to
             WHERE $where
             ORDER BY r.id DESC
             LIMIT $limit"
        );
        $stmt->execute($params);
        return $stmt->fetchAll();
    }

    public function assign(int $relocId, int $assignedTo, int $userId): array
    {
        $db = Database::getConnection();
        $cur = $this->findById($relocId);
        if (!$cur) throw new \RuntimeException('Reubicación no encontrada');
        if (in_array($cur['status'], ['COMPLETED','CANCELLED'], true)) {
            throw new \RuntimeException('Reubicación en estado ' . $cur['status']);
        }

        $db->beginTransaction();
        try {
            $db->prepare("UPDATE relocations SET assigned_to=?, status='ASSIGNED' WHERE id=?")
               ->execute([$assignedTo, $relocId]);
            if ($cur['task_id']) {
                $db->prepare("UPDATE warehouse_tasks SET assigned_to=?, status='ASSIGNED' WHERE id=?")
                   ->execute([$assignedTo, $cur['task_id']]);
            }
            $db->commit();

            \WMS\Services\AuditService::log('relocations', $relocId, 'RELOCATION_ASSIGN', $cur, [
                'assigned_to' => $assignedTo,
            ], $userId);

            return $this->findById($relocId);
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }

    /**
     * Ejecuta el movimiento físico: mueve todo el stock de la LPN del from_bin al to_bin.
     */
    public function complete(int $relocId, int $userId): array
    {
        $cur = $this->findById($relocId);
        if (!$cur) throw new \RuntimeException('Reubicación no encontrada');
        if ($cur['status'] === 'COMPLETED') throw new \RuntimeException('Ya completada');
        if ($cur['status'] === 'CANCELLED') throw new \RuntimeException('Cancelada — no se puede completar');

        $db = Database::getConnection();
        $db->beginTransaction();
        try {
            // Buscar stock asociado a la LPN en el bin origen
            $stmt = $db->prepare(
                "SELECT s.id, s.item_id, s.batch_id, s.quantity, s.reserved_qty,
                        s.stock_status, s.uom
                 FROM container_lines cl
                 JOIN stock s ON s.item_id = cl.item_id
                              AND (s.batch_id <=> cl.batch_id)
                              AND s.warehouse_id = ?
                              AND s.bin_id = ?
                              AND s.quantity > 0
                 WHERE cl.container_id = ?
                 FOR UPDATE"
            );
            $stmt->execute([$cur['warehouse_id'], $cur['from_bin_id'], $cur['container_id']]);
            $positions = $stmt->fetchAll();

            if (empty($positions)) {
                throw new \RuntimeException('No hay stock para mover en el bin origen');
            }

            $movedQty = 0;
            foreach ($positions as $p) {
                $qty = (float) $p['quantity'];
                if ($qty <= 0) continue;

                $newStockId = $this->stockRepo->findOrCreatePosition(
                    (int)$cur['warehouse_id'],
                    (int)$cur['to_bin_id'],
                    (int)$p['item_id'],
                    $p['batch_id'] !== null ? (int)$p['batch_id'] : null,
                    $p['stock_status'],
                    $p['uom']
                );
                $this->stockRepo->addQuantity($newStockId, $qty);
                $this->stockRepo->subtractQuantity((int)$p['id'], $qty);

                // Trasladar reserva si la hay
                if ((float)$p['reserved_qty'] > 0) {
                    $db->prepare("UPDATE stock SET reserved_qty = reserved_qty + ? WHERE id = ?")
                       ->execute([$p['reserved_qty'], $newStockId]);
                }

                $this->movementRepo->logMovement([
                    'warehouse_id'   => (int)$cur['warehouse_id'],
                    'movement_type'  => 'MOVE',
                    'item_id'        => (int)$p['item_id'],
                    'batch_id'       => $p['batch_id'] !== null ? (int)$p['batch_id'] : null,
                    'from_bin_id'    => (int)$cur['from_bin_id'],
                    'to_bin_id'      => (int)$cur['to_bin_id'],
                    'from_status'    => $p['stock_status'],
                    'to_status'      => $p['stock_status'],
                    'quantity'       => $qty,
                    'uom'            => $p['uom'],
                    'reference_type' => 'RELOCATION',
                    'reference_id'   => $relocId,
                    'reason'         => 'Reubicación ' . $cur['relocation_number'],
                    'created_by'     => $userId,
                ]);

                $movedQty += $qty;
            }

            $db->prepare(
                "UPDATE relocations
                 SET status='COMPLETED', completed_at=NOW(), completed_by=?
                 WHERE id=?"
            )->execute([$userId, $relocId]);

            if ($cur['task_id']) {
                $db->prepare(
                    "UPDATE warehouse_tasks
                     SET status='COMPLETED', completed_at=NOW(), confirmed_qty=?
                     WHERE id=?"
                )->execute([$movedQty, $cur['task_id']]);
            }

            $db->commit();

            \WMS\Services\AuditService::log('relocations', $relocId, 'RELOCATION_COMPLETE', $cur, [
                'moved_qty'   => $movedQty,
                'positions'   => count($positions),
            ], $userId);

            return $this->findById($relocId);
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }

    public function cancel(int $relocId, ?string $reason, int $userId): array
    {
        $cur = $this->findById($relocId);
        if (!$cur) throw new \RuntimeException('Reubicación no encontrada');
        if ($cur['status'] === 'COMPLETED') throw new \RuntimeException('Ya completada — no se puede cancelar');
        if ($cur['status'] === 'CANCELLED') throw new \RuntimeException('Ya cancelada');

        $db = Database::getConnection();
        $db->beginTransaction();
        try {
            $db->prepare(
                "UPDATE relocations
                 SET status='CANCELLED', cancelled_at=NOW(), cancelled_reason=?
                 WHERE id=?"
            )->execute([$reason, $relocId]);

            if ($cur['task_id']) {
                $db->prepare("UPDATE warehouse_tasks SET status='CANCELLED' WHERE id=?")
                   ->execute([$cur['task_id']]);
            }
            $db->commit();

            \WMS\Services\AuditService::log('relocations', $relocId, 'RELOCATION_CANCEL', $cur, [
                'reason' => $reason,
            ], $userId);

            return $this->findById($relocId);
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }
}
