<?php

namespace WMS\Services;

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

/**
 * Servicio de Conteo Cíclico.
 *
 * Flujo:
 * 1. Se crea un conteo para un almacén
 * 2. Se agregan bins/items a contar (automático o manual)
 * 3. El operador registra la cantidad contada
 * 4. El sistema calcula la varianza
 * 5. Un supervisor aprueba el ajuste
 * 6. Se genera movimiento de ADJUSTMENT
 */
class CycleCountService
{
    private StockRepository $stockRepo;
    private StockMovementRepository $movementRepo;

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

    /**
     * Crea un nuevo conteo cíclico.
     */
    public function createCount(array $data, int $userId): array
    {
        $db = Database::getConnection();
        $countNumber = Database::nextDocNumber('COUNT');

        $stmt = $db->prepare(
            "INSERT INTO cycle_counts
             (count_number, warehouse_id, count_type, status, planned_date, assigned_to, notes, created_by)
             VALUES (?, ?, ?, 'PLANNED', ?, ?, ?, ?)"
        );
        $stmt->execute([
            $countNumber,
            $data['warehouse_id'],
            $data['count_type'] ?? 'PARTIAL',
            $data['planned_date'] ?? date('Y-m-d'),
            $data['assigned_to'] ?? null,
            $data['notes'] ?? null,
            $userId,
        ]);
        $countId = (int) $db->lastInsertId();

        // Si se indicaron bins, generar líneas de conteo
        if (!empty($data['bin_ids'])) {
            $this->generateCountLines($countId, $data['warehouse_id'], $data['bin_ids']);
        }

        return $this->getCount($countId);
    }

    /**
     * Registra la cantidad contada por el operador.
     */
    public function recordCount(int $lineId, float $countedQty, int $userId): array
    {
        $db = Database::getConnection();

        $stmt = $db->prepare("SELECT * FROM cycle_count_lines WHERE id = ?");
        $stmt->execute([$lineId]);
        $line = $stmt->fetch();
        if (!$line) {
            throw new \RuntimeException('Línea de conteo no encontrada');
        }

        $variance = $countedQty - $line['system_qty'];

        $db->prepare(
            "UPDATE cycle_count_lines
             SET counted_qty = ?, variance = ?, status = 'COUNTED',
                 counted_by = ?, counted_at = NOW()
             WHERE id = ?"
        )->execute([$countedQty, $variance, $userId, $lineId]);

        // Actualizar estado del conteo padre a IN_PROGRESS
        $db->prepare(
            "UPDATE cycle_counts SET status = 'IN_PROGRESS', started_at = COALESCE(started_at, NOW())
             WHERE id = ? AND status IN ('PLANNED','IN_PROGRESS')"
        )->execute([$line['cycle_count_id']]);

        $stmt = $db->prepare("SELECT * FROM cycle_count_lines WHERE id = ?");
        $stmt->execute([$lineId]);
        return $stmt->fetch();
    }

    /**
     * Aprueba un ajuste de conteo y genera movimiento de stock.
     */
    public function approveAdjustment(int $lineId, int $userId): array
    {
        $db = Database::getConnection();

        $stmt = $db->prepare(
            "SELECT ccl.*, cc.warehouse_id FROM cycle_count_lines ccl
             JOIN cycle_counts cc ON ccl.cycle_count_id = cc.id
             WHERE ccl.id = ?"
        );
        $stmt->execute([$lineId]);
        $line = $stmt->fetch();

        if (!$line || $line['status'] !== 'COUNTED') {
            throw new \RuntimeException('Línea no válida para aprobación');
        }

        if ($line['variance'] == 0) {
            // Sin varianza, solo marcar como aprobado
            $db->prepare(
                "UPDATE cycle_count_lines SET status = 'APPROVED', approved_by = ?, approved_at = NOW() WHERE id = ?"
            )->execute([$userId, $lineId]);
        } else {
            $db->beginTransaction();
            try {
                // Ajustar stock
                $stockId = $this->stockRepo->findOrCreatePosition(
                    $line['warehouse_id'], $line['bin_id'], $line['item_id'], $line['batch_id'], 'AVAILABLE'
                );

                if ($line['variance'] > 0) {
                    $this->stockRepo->addQuantity($stockId, abs($line['variance']));
                } else {
                    $this->stockRepo->subtractQuantity($stockId, abs($line['variance']));
                }

                // Registrar movimiento de ajuste
                $this->movementRepo->logMovement([
                    'warehouse_id'  => $line['warehouse_id'],
                    'movement_type' => 'ADJUSTMENT',
                    'item_id'       => $line['item_id'],
                    'batch_id'      => $line['batch_id'],
                    'from_bin_id'   => $line['bin_id'],
                    'to_bin_id'     => $line['bin_id'],
                    'quantity'      => abs($line['variance']),
                    'reason'        => 'Ajuste por conteo cíclico. Varianza: ' . $line['variance'],
                    'reference_type'=> 'CYCLE_COUNT',
                    'reference_id'  => $line['cycle_count_id'],
                    'created_by'    => $userId,
                ]);

                $db->prepare(
                    "UPDATE cycle_count_lines SET status = 'ADJUSTED', approved_by = ?, approved_at = NOW() WHERE id = ?"
                )->execute([$userId, $lineId]);

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

        $stmt = $db->prepare("SELECT * FROM cycle_count_lines WHERE id = ?");
        $stmt->execute([$lineId]);
        return $stmt->fetch();
    }

    /**
     * Genera líneas de conteo para los bins indicados.
     */
    private function generateCountLines(int $countId, int $warehouseId, array $binIds): void
    {
        $db = Database::getConnection();

        foreach ($binIds as $binId) {
            // Obtener stock actual en ese bin
            $stocks = $this->stockRepo->getStockByBin($binId);

            foreach ($stocks as $stock) {
                $db->prepare(
                    "INSERT INTO cycle_count_lines
                     (cycle_count_id, bin_id, item_id, batch_id, system_qty, status)
                     VALUES (?, ?, ?, ?, ?, 'PENDING')"
                )->execute([
                    $countId,
                    $binId,
                    $stock['item_id'],
                    $stock['batch_id'],
                    $stock['quantity'],
                ]);
            }
        }
    }

    /**
     * Crea conteo cíclico seleccionando bins específicos.
     * Genera líneas a partir del stock existente en esos bins.
     */
    public function createByLocations(array $data, int $userId): array
    {
        $db = Database::getConnection();
        $countNumber = Database::nextDocNumber('COUNT');

        $stmt = $db->prepare(
            "INSERT INTO cycle_counts
             (count_number, warehouse_id, count_type, count_mode, count_rounds, status, planned_date, created_by)
             VALUES (?, ?, 'BY_LOCATION', ?, ?, 'PLANNED', ?, ?)"
        );
        $stmt->execute([
            $countNumber,
            $data['warehouse_id'],
            $data['count_mode'],
            $data['count_rounds'] ?? 1,
            date('Y-m-d'),
            $userId,
        ]);
        $countId = (int) $db->lastInsertId();

        // Generar líneas desde stock en los bins indicados
        $this->generateCountLines($countId, (int) $data['warehouse_id'], $data['bin_ids']);

        return $this->getCount($countId);
    }

    /**
     * Crea conteo cíclico desde registros de stock de items específicos.
     */
    public function createByStockRecords(array $data, int $userId): array
    {
        $db = Database::getConnection();
        $countNumber = Database::nextDocNumber('COUNT');

        $stmt = $db->prepare(
            "INSERT INTO cycle_counts
             (count_number, warehouse_id, count_type, count_mode, count_rounds, status, planned_date, created_by)
             VALUES (?, ?, 'BY_STOCK', ?, ?, 'PLANNED', ?, ?)"
        );
        $stmt->execute([
            $countNumber,
            $data['warehouse_id'],
            $data['count_mode'],
            $data['count_rounds'] ?? 1,
            date('Y-m-d'),
            $userId,
        ]);
        $countId = (int) $db->lastInsertId();

        // Generar líneas desde stock de los items indicados
        foreach ($data['item_ids'] as $itemId) {
            $stocks = $this->stockRepo->getStockByItem((int) $itemId, (int) $data['warehouse_id']);
            foreach ($stocks as $stock) {
                $db->prepare(
                    "INSERT INTO cycle_count_lines
                     (cycle_count_id, bin_id, item_id, batch_id, system_qty, status)
                     VALUES (?, ?, ?, ?, ?, 'PENDING')"
                )->execute([
                    $countId,
                    $stock['bin_id'],
                    $stock['item_id'],
                    $stock['batch_id'],
                    $stock['quantity'],
                ]);
            }
        }

        return $this->getCount($countId);
    }

    /**
     * Registra segundo conteo para modos TWO_CONSECUTIVE/TWO_ANY.
     * Compara con primer conteo y calcula varianza final.
     */
    public function recordSecondCount(int $lineId, float $countedQty, int $userId): array
    {
        $db = Database::getConnection();

        $stmt = $db->prepare(
            "SELECT ccl.*, cc.count_mode FROM cycle_count_lines ccl
             JOIN cycle_counts cc ON ccl.cycle_count_id = cc.id
             WHERE ccl.id = ?"
        );
        $stmt->execute([$lineId]);
        $line = $stmt->fetch();
        if (!$line) {
            throw new \RuntimeException('Línea de conteo no encontrada');
        }

        if ($line['status'] !== 'COUNTED') {
            throw new \RuntimeException('La línea debe tener un primer conteo registrado antes del segundo');
        }

        if (!in_array($line['count_mode'], ['TWO_CONSECUTIVE', 'TWO_ANY'])) {
            throw new \RuntimeException('El modo de conteo no requiere segundo conteo');
        }

        $variance = $countedQty - $line['system_qty'];

        // Si ambos conteos coinciden, se considera confirmado
        $matchesFirst = ((float) $line['counted_qty'] === $countedQty);
        $newStatus = $matchesFirst ? 'SECOND_COUNTED' : 'DISCREPANCY';

        $db->prepare(
            "UPDATE cycle_count_lines
             SET second_counted_qty = ?, second_variance = ?, status = ?,
                 second_counted_by = ?, second_counted_at = NOW()
             WHERE id = ?"
        )->execute([$countedQty, $variance, $newStatus, $userId, $lineId]);

        $stmt = $db->prepare("SELECT * FROM cycle_count_lines WHERE id = ?");
        $stmt->execute([$lineId]);
        return $stmt->fetch();
    }

    /**
     * Ajuste directo sin planificación previa.
     * Ajusta stock inmediatamente y sincroniza con SAP.
     */
    public function directAdjustment(array $data, int $userId): array
    {
        $db = Database::getConnection();
        $db->beginTransaction();

        try {
            $adjNumber = Database::nextDocNumber('DADJ');

            // Resolver item por código
            $itemRepo = new ItemRepository();
            $stmtItem = $db->prepare("SELECT id FROM items WHERE item_code = ?");
            $stmtItem->execute([$data['item_code']]);
            $item = $stmtItem->fetch();
            if (!$item) {
                throw new \RuntimeException('Artículo no encontrado: ' . $data['item_code']);
            }
            $itemId = (int) $item['id'];

            // Resolver batch si se proporcionó
            $batchId = null;
            if (!empty($data['batch_number'])) {
                $batchRepo = new BatchRepository();
                $batch = $batchRepo->findByItemAndNumber($itemId, $data['batch_number']);
                if (!$batch) {
                    throw new \RuntimeException('Lote no encontrado: ' . $data['batch_number']);
                }
                $batchId = (int) $batch['id'];
            }

            $warehouseId = (int) $data['warehouse_id'];
            $binId       = (int) $data['bin_id'];
            $qty         = (float) $data['quantity'];
            $isPositive  = ($data['adjustment_type'] === 'POSITIVE');

            // Ajustar stock
            $stockId = $this->stockRepo->findOrCreatePosition(
                $warehouseId, $binId, $itemId, $batchId, 'AVAILABLE'
            );

            if ($isPositive) {
                $this->stockRepo->addQuantity($stockId, $qty);
            } else {
                $this->stockRepo->subtractQuantity($stockId, $qty);
            }

            // Registrar movimiento
            $this->movementRepo->logMovement([
                'warehouse_id'   => $warehouseId,
                'movement_type'  => 'DIRECT_ADJUSTMENT',
                'item_id'        => $itemId,
                'batch_id'       => $batchId,
                'from_bin_id'    => $binId,
                'to_bin_id'      => $binId,
                'quantity'       => $qty,
                'reason'         => "Ajuste directo {$adjNumber}: " . $data['reason'],
                'reference_type' => 'DIRECT_ADJUSTMENT',
                'reference_id'   => null,
                'created_by'     => $userId,
            ]);

            // Registrar documento de ajuste
            $db->prepare(
                "INSERT INTO direct_adjustments
                 (adjustment_number, warehouse_id, bin_id, item_id, batch_id,
                  adjustment_type, quantity, reason, status, created_by)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'APPLIED', ?)"
            )->execute([
                $adjNumber, $warehouseId, $binId, $itemId, $batchId,
                $data['adjustment_type'], $qty, $data['reason'], $userId,
            ]);
            $adjId = (int) $db->lastInsertId();

            $db->commit();

            // Sincronizar con SAP (fuera de transacción)
            try {
                $sap = new SapServiceLayerClient();
                $sapPayload = [
                    'DocumentLines' => [[
                        'ItemCode'      => $data['item_code'],
                        'Quantity'      => $qty,
                        'WarehouseCode' => $this->getWarehouseCode($warehouseId),
                    ]],
                ];
                if (!empty($data['batch_number'])) {
                    $sapPayload['DocumentLines'][0]['BatchNumbers'] = [[
                        'BatchNumber' => $data['batch_number'],
                        'Quantity'    => $qty,
                    ]];
                }

                if ($isPositive) {
                    $sapResult = $sap->post('/InventoryGenEntries', $sapPayload);
                } else {
                    $sapResult = $sap->post('/InventoryGenExits', $sapPayload);
                }

                $db->prepare(
                    "UPDATE direct_adjustments SET sap_doc_entry = ?, sap_synced_at = NOW() WHERE id = ?"
                )->execute([$sapResult['DocEntry'] ?? null, $adjId]);
            } catch (\Throwable $e) {
                // Log SAP error but don't rollback local adjustment
                error_log("SAP sync failed for direct adjustment {$adjNumber}: " . $e->getMessage());
            }

            return [
                'id'                => $adjId,
                'adjustment_number' => $adjNumber,
                'adjustment_type'   => $data['adjustment_type'],
                'quantity'          => $qty,
                'item_code'         => $data['item_code'],
                'status'            => 'APPLIED',
            ];
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }

    /**
     * Obtiene el código de almacén por ID.
     */
    private function getWarehouseCode(int $warehouseId): string
    {
        $db = Database::getConnection();
        $stmt = $db->prepare("SELECT code FROM warehouses WHERE id = ?");
        $stmt->execute([$warehouseId]);
        $row = $stmt->fetch();
        return $row ? $row['code'] : '';
    }

    public function getCount(int $countId): ?array
    {
        $db = Database::getConnection();

        $stmt = $db->prepare("SELECT * FROM cycle_counts WHERE id = ?");
        $stmt->execute([$countId]);
        $count = $stmt->fetch();
        if (!$count) return null;

        $stmt = $db->prepare(
            "SELECT ccl.*, i.item_code, i.item_name, b.code AS bin_code, bt.batch_number
             FROM cycle_count_lines ccl
             JOIN items i ON ccl.item_id = i.id
             JOIN bins b ON ccl.bin_id = b.id
             LEFT JOIN batches bt ON ccl.batch_id = bt.id
             WHERE ccl.cycle_count_id = ?
             ORDER BY b.code, i.item_code"
        );
        $stmt->execute([$countId]);
        $count['lines'] = $stmt->fetchAll();

        return $count;
    }
}
