<?php

namespace WMS\Services;

use WMS\Core\Database;
use WMS\Repositories\StockRepository;
use WMS\Repositories\StockMovementRepository;
use WMS\Repositories\BatchRepository;
use WMS\Repositories\BinRepository;
use WMS\Services\AuditService;

/**
 * Servicio de Control de Calidad (QA).
 *
 * Flujo:
 * 1. Stock llega con estado QA (cuarentena) desde recepción
 * 2. Inspector revisa el lote y decide: aprobar, rechazar o mantener en cuarentena
 * 3. Aprobación: stock pasa de QA a AVAILABLE, lote pasa a AVAILABLE
 * 4. Rechazo: stock pasa de QA a BLOCKED, lote pasa a REJECTED
 * 5. Cuarentena extendida: stock permanece QA, se registra motivo
 * 6. Todas las decisiones se registran en qa_decisions y stock_movements
 */
class QaService
{
    private StockRepository $stockRepo;
    private StockMovementRepository $movementRepo;
    private BatchRepository $batchRepo;
    private BinRepository $binRepo;

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

    /**
     * Aprueba un lote: stock QA -> AVAILABLE.
     * Opcionalmente mueve el stock a un bin de almacenamiento.
     */
    public function approveBatch(int $batchId, array $data, int $userId): array
    {
        $db = Database::getConnection();
        $db->beginTransaction();

        try {
            $batch = $this->batchRepo->findById($batchId);
            if (!$batch) {
                throw new \RuntimeException('Lote no encontrado');
            }

            // Obtener posiciones de stock en QA para este lote
            $stockPositions = $this->getQaStockForBatch($batchId);
            if (empty($stockPositions)) {
                throw new \RuntimeException('No hay stock en estado QA para este lote');
            }

            $targetBinId = $data['target_bin_id'] ?? null;

            foreach ($stockPositions as $position) {
                $qty = (float) $position['quantity'];
                if ($qty <= 0) {
                    continue;
                }

                if ($targetBinId) {
                    // Mover a nuevo bin con estado AVAILABLE
                    $newStockId = $this->stockRepo->findOrCreatePosition(
                        $position['warehouse_id'],
                        $targetBinId,
                        $position['item_id'],
                        $position['batch_id'],
                        'AVAILABLE',
                        $position['uom']
                    );
                    $this->stockRepo->addQuantity($newStockId, $qty);
                    $this->stockRepo->subtractQuantity($position['id'], $qty);

                    // Registrar movimiento con cambio de bin
                    $this->movementRepo->logMovement([
                        'warehouse_id'   => $position['warehouse_id'],
                        'movement_type'  => 'QA_APPROVE',
                        'item_id'        => $position['item_id'],
                        'batch_id'       => $position['batch_id'],
                        'from_bin_id'    => $position['bin_id'],
                        'from_status'    => 'QA',
                        'to_bin_id'      => $targetBinId,
                        'to_status'      => 'AVAILABLE',
                        'quantity'       => $qty,
                        'uom'            => $position['uom'],
                        'reference_type' => 'QA_DECISION',
                        'reference_id'   => $batchId,
                        'created_by'     => $userId,
                    ]);
                } else {
                    // Cambiar estado in-place: QA -> AVAILABLE
                    $newStockId = $this->stockRepo->findOrCreatePosition(
                        $position['warehouse_id'],
                        $position['bin_id'],
                        $position['item_id'],
                        $position['batch_id'],
                        'AVAILABLE',
                        $position['uom']
                    );
                    $this->stockRepo->addQuantity($newStockId, $qty);
                    $this->stockRepo->subtractQuantity($position['id'], $qty);

                    // Registrar movimiento de cambio de estado
                    $this->movementRepo->logMovement([
                        'warehouse_id'   => $position['warehouse_id'],
                        'movement_type'  => 'QA_APPROVE',
                        'item_id'        => $position['item_id'],
                        'batch_id'       => $position['batch_id'],
                        'from_bin_id'    => $position['bin_id'],
                        'from_status'    => 'QA',
                        'to_bin_id'      => $position['bin_id'],
                        'to_status'      => 'AVAILABLE',
                        'quantity'       => $qty,
                        'uom'            => $position['uom'],
                        'reference_type' => 'QA_DECISION',
                        'reference_id'   => $batchId,
                        'created_by'     => $userId,
                    ]);
                }
            }

            // Actualizar estado del lote
            $this->batchRepo->updateStatus($batchId, 'AVAILABLE');

            // Registrar decisión QA
            $decisionId = $this->logQaDecision($db, [
                'batch_id'    => $batchId,
                'decision'    => 'APPROVED',
                'reason'      => $data['reason'] ?? null,
                'notes'       => $data['notes'] ?? null,
                'decided_by'  => $userId,
            ]);

            // Generar tareas de Putaway automáticas para stock que quedó en bin RECEIVING.
            // El stock recién aprobado debe moverse al bin de almacenamiento definitivo.
            $putawayTasks = [];
            $stmtPut = $db->prepare(
                "SELECT s.id AS stock_id, s.warehouse_id, s.bin_id, s.item_id, s.batch_id, s.quantity, s.uom
                 FROM stock s
                 JOIN bins bn ON bn.id = s.bin_id
                 WHERE s.batch_id = ? AND s.stock_status = 'AVAILABLE' AND s.quantity > 0
                   AND bn.bin_type = 'RECEIVING'"
            );
            $stmtPut->execute([$batchId]);
            foreach ($stmtPut->fetchAll() as $sp) {
                // Evitar duplicar si ya existe tarea PUT pendiente para este stock
                $stmtChk = $db->prepare(
                    "SELECT id FROM warehouse_tasks
                     WHERE task_type = 'PUTAWAY' AND batch_id = ? AND from_bin_id = ?
                       AND status IN ('PENDING','ASSIGNED','IN_PROGRESS') LIMIT 1"
                );
                $stmtChk->execute([$batchId, $sp['bin_id']]);
                if ($stmtChk->fetch()) continue;

                $stmtIns = $db->prepare(
                    "INSERT INTO warehouse_tasks
                     (warehouse_id, task_type, priority, item_id, batch_id, from_bin_id, quantity,
                      reference_type, reference_id, status, created_by)
                     VALUES (?, 'PUTAWAY', 2, ?, ?, ?, ?, 'QA_DECISION', ?, 'PENDING', ?)"
                );
                $stmtIns->execute([
                    $sp['warehouse_id'], $sp['item_id'], $batchId, $sp['bin_id'],
                    $sp['quantity'], $decisionId, $userId,
                ]);
                $taskId = (int) $db->lastInsertId();
                $putawayTasks[] = $taskId;
            }

            $db->commit();

            AuditService::log('qa_decisions', $decisionId, 'QA_APPROVE', [
                'batch_status_prev' => $batch['status'] ?? null,
            ], [
                'batch_id'      => $batchId,
                'batch_number'  => $batch['batch_number'] ?? null,
                'target_bin_id' => $targetBinId,
                'positions'     => count($stockPositions),
                'reason'        => $data['reason'] ?? null,
                'notes'         => $data['notes'] ?? null,
            ], $userId);
            // Espejo sobre batches para timeline del lote en módulo QA
            AuditService::log('batches', $batchId, 'BATCH_QA_APPROVED', [
                'batch_status_prev' => $batch['status'] ?? null,
            ], [
                'decision_id'      => $decisionId,
                'batch_number'     => $batch['batch_number'] ?? null,
                'target_bin_id'    => $targetBinId,
                'positions'        => count($stockPositions),
                'reason'           => $data['reason'] ?? null,
                'notes'            => $data['notes'] ?? null,
                'putaway_tasks'    => $putawayTasks,
            ], $userId);

            // Auditar cada tarea PUT generada
            foreach ($putawayTasks as $tid) {
                AuditService::log('warehouse_tasks', $tid, 'PUT_TASK_AUTO_CREATED', null, [
                    'origin'      => 'QA_APPROVE',
                    'batch_id'    => $batchId,
                    'decision_id' => $decisionId,
                ], $userId);
            }

            return [
                'batch_id'      => $batchId,
                'decision'      => 'APPROVED',
                'batch'         => $this->batchRepo->findById($batchId),
                'putaway_tasks' => $putawayTasks,
            ];
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }

    /**
     * Rechaza un lote: stock QA -> BLOCKED.
     * El stock se mueve a un bin de rechazados si está configurado.
     */
    public function rejectBatch(int $batchId, array $data, int $userId): array
    {
        $db = Database::getConnection();
        $db->beginTransaction();

        try {
            $batch = $this->batchRepo->findById($batchId);
            if (!$batch) {
                throw new \RuntimeException('Lote no encontrado');
            }

            if (empty($data['reason'])) {
                throw new \RuntimeException('El motivo de rechazo es obligatorio');
            }

            $stockPositions = $this->getQaStockForBatch($batchId);
            if (empty($stockPositions)) {
                throw new \RuntimeException('No hay stock en estado QA para este lote');
            }

            $targetBinId = $data['target_bin_id'] ?? null;

            foreach ($stockPositions as $position) {
                $qty = (float) $position['quantity'];
                if ($qty <= 0) {
                    continue;
                }

                $destBinId = $targetBinId ?? $position['bin_id'];

                // Crear posición BLOCKED
                $newStockId = $this->stockRepo->findOrCreatePosition(
                    $position['warehouse_id'],
                    $destBinId,
                    $position['item_id'],
                    $position['batch_id'],
                    'BLOCKED',
                    $position['uom']
                );
                $this->stockRepo->addQuantity($newStockId, $qty);
                $this->stockRepo->subtractQuantity($position['id'], $qty);

                // Registrar movimiento
                $this->movementRepo->logMovement([
                    'warehouse_id'   => $position['warehouse_id'],
                    'movement_type'  => 'QA_REJECT',
                    'item_id'        => $position['item_id'],
                    'batch_id'       => $position['batch_id'],
                    'from_bin_id'    => $position['bin_id'],
                    'from_status'    => 'QA',
                    'to_bin_id'      => $destBinId,
                    'to_status'      => 'BLOCKED',
                    'quantity'       => $qty,
                    'uom'            => $position['uom'],
                    'reference_type' => 'QA_DECISION',
                    'reference_id'   => $batchId,
                    'created_by'     => $userId,
                ]);
            }

            // Actualizar estado del lote
            $this->batchRepo->updateStatus($batchId, 'REJECTED');

            // Registrar decisión QA
            $decisionId = $this->logQaDecision($db, [
                'batch_id'    => $batchId,
                'decision'    => 'REJECTED',
                'reason'      => $data['reason'],
                'notes'       => $data['notes'] ?? null,
                'decided_by'  => $userId,
            ]);

            $db->commit();

            AuditService::log('qa_decisions', $decisionId, 'QA_REJECT', [
                'batch_status_prev' => $batch['status'] ?? null,
            ], [
                'batch_id'      => $batchId,
                'batch_number'  => $batch['batch_number'] ?? null,
                'target_bin_id' => $targetBinId,
                'positions'     => count($stockPositions),
                'reason'        => $data['reason'],
                'notes'         => $data['notes'] ?? null,
            ], $userId);
            AuditService::log('batches', $batchId, 'BATCH_QA_REJECTED', [
                'batch_status_prev' => $batch['status'] ?? null,
            ], [
                'decision_id'   => $decisionId,
                'batch_number'  => $batch['batch_number'] ?? null,
                'target_bin_id' => $targetBinId,
                'positions'     => count($stockPositions),
                'reason'        => $data['reason'],
                'notes'         => $data['notes'] ?? null,
            ], $userId);

            return [
                'batch_id' => $batchId,
                'decision' => 'REJECTED',
                'batch'    => $this->batchRepo->findById($batchId),
            ];
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }

    /**
     * Mantiene un lote en cuarentena extendida.
     * Stock permanece en QA pero se registra el motivo y la revisión.
     */
    public function quarantineBatch(int $batchId, array $data, int $userId): array
    {
        $db = Database::getConnection();
        $db->beginTransaction();

        try {
            $batch = $this->batchRepo->findById($batchId);
            if (!$batch) {
                throw new \RuntimeException('Lote no encontrado');
            }

            if (empty($data['reason'])) {
                throw new \RuntimeException('El motivo de cuarentena extendida es obligatorio');
            }

            $stockPositions = $this->getQaStockForBatch($batchId);
            if (empty($stockPositions)) {
                throw new \RuntimeException('No hay stock en estado QA para este lote');
            }

            // Registrar movimiento informativo (sin cambio de estado)
            foreach ($stockPositions as $position) {
                $qty = (float) $position['quantity'];
                if ($qty <= 0) {
                    continue;
                }

                $this->movementRepo->logMovement([
                    'warehouse_id'   => $position['warehouse_id'],
                    'movement_type'  => 'QA_QUARANTINE',
                    'item_id'        => $position['item_id'],
                    'batch_id'       => $position['batch_id'],
                    'from_bin_id'    => $position['bin_id'],
                    'from_status'    => 'QA',
                    'to_bin_id'      => $position['bin_id'],
                    'to_status'      => 'QA',
                    'quantity'       => $qty,
                    'uom'            => $position['uom'],
                    'reference_type' => 'QA_DECISION',
                    'reference_id'   => $batchId,
                    'created_by'     => $userId,
                ]);
            }

            // Registrar decisión QA
            $decisionId = $this->logQaDecision($db, [
                'batch_id'       => $batchId,
                'decision'       => 'QUARANTINE',
                'reason'         => $data['reason'],
                'notes'          => $data['notes'] ?? null,
                'review_date'    => $data['review_date'] ?? null,
                'decided_by'     => $userId,
            ]);

            $db->commit();

            AuditService::log('qa_decisions', $decisionId, 'QA_QUARANTINE', null, [
                'batch_id'     => $batchId,
                'batch_number' => $batch['batch_number'] ?? null,
                'positions'    => count($stockPositions),
                'reason'       => $data['reason'],
                'notes'        => $data['notes'] ?? null,
                'review_date'  => $data['review_date'] ?? null,
            ], $userId);
            AuditService::log('batches', $batchId, 'BATCH_QA_QUARANTINED', null, [
                'decision_id'  => $decisionId,
                'batch_number' => $batch['batch_number'] ?? null,
                'positions'    => count($stockPositions),
                'reason'       => $data['reason'],
                'notes'        => $data['notes'] ?? null,
                'review_date'  => $data['review_date'] ?? null,
            ], $userId);

            return [
                'batch_id' => $batchId,
                'decision' => 'QUARANTINE',
                'batch'    => $this->batchRepo->findById($batchId),
            ];
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }

    /**
     * Obtiene posiciones de stock en estado QA para un lote dado.
     */
    private function getQaStockForBatch(int $batchId): array
    {
        $db = Database::getConnection();
        $stmt = $db->prepare(
            "SELECT * FROM stock WHERE batch_id = ? AND stock_status = 'QA' AND quantity > 0"
        );
        $stmt->execute([$batchId]);
        return $stmt->fetchAll();
    }

    /**
     * Registra una decisión de QA en la tabla qa_decisions.
     */
    private function logQaDecision(\PDO $db, array $data): int
    {
        $stmt = $db->prepare(
            "INSERT INTO qa_decisions (batch_id, decision, reason, notes, review_date, decided_by)
             VALUES (?, ?, ?, ?, ?, ?)"
        );
        $stmt->execute([
            $data['batch_id'],
            $data['decision'],
            $data['reason'] ?? null,
            $data['notes'] ?? null,
            $data['review_date'] ?? null,
            $data['decided_by'],
        ]);
        return (int) $db->lastInsertId();
    }

    /**
     * Genera tareas de PUTAWAY para stock en QA SIN liberar el lote.
     * El stock permanece en QA en el bin destino: el operador físicamente lo
     * almacena pero queda bloqueado para picking hasta que QA lo apruebe.
     * Se auto-asigna a un usuario op_almacen disponible (round-robin por carga).
     */
    public function sendToPutaway(int $batchId, int $userId): array
    {
        $db = Database::getConnection();

        $batch = $this->batchRepo->findById($batchId);
        if (!$batch) {
            throw new \RuntimeException('Lote no encontrado');
        }

        // Stock en QA dentro de un bin RECEIVING (lo que está esperando aprobación)
        $stmt = $db->prepare(
            "SELECT s.warehouse_id, s.bin_id, s.item_id, s.batch_id, s.quantity, s.uom
             FROM stock s JOIN bins b ON b.id = s.bin_id
             WHERE s.batch_id = ? AND s.stock_status = 'QA' AND s.quantity > 0
               AND b.bin_type = 'RECEIVING'"
        );
        $stmt->execute([$batchId]);
        $positions = $stmt->fetchAll();
        if (empty($positions)) {
            throw new \RuntimeException('No hay stock en QA dentro de un bin de RECEIVING para este lote');
        }

        $db->beginTransaction();
        $createdTasks = [];
        try {
            foreach ($positions as $sp) {
                // Skip si ya existe tarea pendiente para este stock
                $chk = $db->prepare(
                    "SELECT id FROM warehouse_tasks
                     WHERE task_type='PUTAWAY' AND batch_id=? AND from_bin_id=?
                       AND status IN ('PENDING','ASSIGNED','IN_PROGRESS') LIMIT 1"
                );
                $chk->execute([$batchId, $sp['bin_id']]);
                if ($chk->fetch()) continue;

                // Asignar al op_almacen con menos tareas pendientes activas
                $assignee = $this->pickAvailableOpAlmacen($db, (int) $sp['warehouse_id']);

                $ins = $db->prepare(
                    "INSERT INTO warehouse_tasks
                     (warehouse_id, task_type, priority, item_id, batch_id, from_bin_id, quantity, uom,
                      reference_type, reference_id, status, assigned_to, notes, created_by)
                     VALUES (?, 'PUTAWAY', 2, ?, ?, ?, ?, ?, 'QA_PUTAWAY_HOLD', ?, ?, ?, ?, ?)"
                );
                $ins->execute([
                    $sp['warehouse_id'], $sp['item_id'], $batchId,
                    $sp['bin_id'], $sp['quantity'], $sp['uom'],
                    $batchId,
                    $assignee ? 'ASSIGNED' : 'PENDING',
                    $assignee,
                    'Putaway en cuarentena (auto): stock almacenado con status QA hasta liberación',
                    $userId,
                ]);
                $createdTasks[] = [
                    'task_id'     => (int) $db->lastInsertId(),
                    'assigned_to' => $assignee,
                    'from_bin_id' => (int) $sp['bin_id'],
                    'quantity'    => (float) $sp['quantity'],
                ];
            }

            $db->commit();
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }

        AuditService::log('batches', $batchId, 'QA_SEND_TO_PUTAWAY_HOLD', null, [
            'batch_number' => $batch['batch_number'] ?? null,
            'tasks_created' => count($createdTasks),
            'tasks' => $createdTasks,
        ], $userId);

        return [
            'batch_id'      => $batchId,
            'tasks_created' => count($createdTasks),
            'tasks'         => $createdTasks,
            'note'          => 'Stock se mantiene en QA. Asignado a op_almacen para almacenamiento físico.',
        ];
    }

    /**
     * Devuelve el id de un usuario op_almacen EN LÍNEA con la menor carga.
     * "En línea" = last_activity_at >= NOW() - 5 minutos.
     * Si no hay nadie en línea, cae al op_almacen activo con menos carga (no online).
     */
    private function pickAvailableOpAlmacen(\PDO $db, int $warehouseId): ?int
    {
        // 1) Buscar online del mismo almacén (o sin almacén asignado)
        $sql = "SELECT u.id, COALESCE(SUM(CASE WHEN t.status IN ('PENDING','ASSIGNED','IN_PROGRESS') THEN 1 ELSE 0 END), 0) AS load_active
                FROM users u
                LEFT JOIN warehouse_tasks t ON t.assigned_to = u.id
                WHERE u.role = 'op_almacen' AND u.is_active = 1
                  AND u.last_activity_at >= (NOW() - INTERVAL 5 MINUTE)
                  AND (u.warehouse_id = ? OR u.warehouse_id IS NULL)
                GROUP BY u.id
                ORDER BY load_active ASC, u.last_activity_at DESC, u.id ASC
                LIMIT 1";
        $stmt = $db->prepare($sql);
        $stmt->execute([$warehouseId]);
        $row = $stmt->fetch();
        if ($row) return (int) $row['id'];

        // 2) Fallback: cualquier op_almacen activo (aunque no esté online ahora)
        $stmt = $db->prepare(
            "SELECT u.id, COALESCE(SUM(CASE WHEN t.status IN ('PENDING','ASSIGNED','IN_PROGRESS') THEN 1 ELSE 0 END), 0) AS load_active
             FROM users u
             LEFT JOIN warehouse_tasks t ON t.assigned_to = u.id
             WHERE u.role = 'op_almacen' AND u.is_active = 1
               AND (u.warehouse_id = ? OR u.warehouse_id IS NULL)
             GROUP BY u.id
             ORDER BY load_active ASC, u.id ASC
             LIMIT 1"
        );
        $stmt->execute([$warehouseId]);
        $row = $stmt->fetch();
        return $row ? (int) $row['id'] : null;
    }
}
