<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Auth;
use App\Core\Database;
use App\Core\Tenant;
use RuntimeException;

class InventoryAdjustmentService
{
    private ?bool $variantTableAvailable = null;
    private ?bool $variantQuantityColumnAvailable = null;
    /** @var array<string,bool> */
    private array $columnAvailabilityCache = [];

    public function request(array $payload): int
    {
        $companyId = (int) (Tenant::companyId() ?? 0);
        $userId = (int) (Auth::id() ?? 0);

        if ($companyId <= 0 || $userId <= 0) {
            throw new RuntimeException('Sesión inválida para solicitar ajuste.');
        }

        $productId = (int) ($payload['producto_id'] ?? 0);
        $variantId = (int) ($payload['variante_id'] ?? 0);
        $branchId = (int) ($payload['sucursal_id'] ?? 0);
        $newStock = round((float) ($payload['stock_nuevo'] ?? 0), 2);
        $reason = trim((string) ($payload['motivo'] ?? ''));

        if ($productId <= 0) {
            throw new RuntimeException('Selecciona un producto válido para ajuste.');
        }

        if ($branchId <= 0) {
            throw new RuntimeException('Selecciona una sucursal válida para ajuste.');
        }

        if ($reason === '' || mb_strlen($reason) < 5) {
            throw new RuntimeException('El motivo del ajuste es obligatorio (mínimo 5 caracteres).');
        }

        if ($newStock < 0) {
            throw new RuntimeException('El stock nuevo no puede ser negativo.');
        }

        return Database::transaction(function () use (
            $companyId,
            $userId,
            $productId,
            $variantId,
            $branchId,
            $newStock,
            $reason
        ): int {
            $pdo = Database::connection();

            $hasVariantTable = $this->hasVariantTable($pdo);
            $hasVariantQty = $hasVariantTable ? $this->hasVariantQuantityColumn($pdo) : false;
            $hasAdjustmentVariantColumn = $this->tableHasColumn($pdo, 'ajustes_inventario', 'variante_id');

            if ($variantId > 0 && !$hasAdjustmentVariantColumn) {
                throw new RuntimeException(
                    'No puedes solicitar ajustes por variación sin columna variante_id en ajustes_inventario. Ejecuta la migración 025.'
                );
            }

            $userStmt = $pdo->prepare(
                'SELECT sucursal_id
                 FROM usuarios
                 WHERE id = :usuario_id
                   AND empresa_id = :empresa_id
                 LIMIT 1'
            );
            $userStmt->execute([
                ':usuario_id' => $userId,
                ':empresa_id' => $companyId,
            ]);
            $assignedBranchId = (int) ($userStmt->fetchColumn() ?: 0);

            if ($assignedBranchId > 0 && $assignedBranchId !== $branchId) {
                throw new RuntimeException('No puedes solicitar ajustes fuera de tu sucursal asignada.');
            }

            $productStmt = $pdo->prepare(
                'SELECT id, nombre, precio_compra
                 FROM productos
                 WHERE id = :producto_id
                   AND empresa_id = :empresa_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $productStmt->execute([
                ':producto_id' => $productId,
                ':empresa_id' => $companyId,
            ]);
            $product = $productStmt->fetch();

            if (!$product) {
                throw new RuntimeException('El producto no existe en la empresa activa.');
            }

            $productVariantCountStmt = null;
            $variantSelectStmt = null;
            if ($hasVariantTable) {
                $productVariantCountStmt = $pdo->prepare(
                    'SELECT COUNT(*)
                     FROM producto_variantes
                     WHERE empresa_id = :empresa_id
                       AND producto_id = :producto_id
                       AND activo = 1'
                );

                $variantSelectStmt = $pdo->prepare(
                    $hasVariantQty
                        ? 'SELECT id, nombre, activo, cantidad
                           FROM producto_variantes
                           WHERE id = :variante_id
                             AND empresa_id = :empresa_id
                             AND producto_id = :producto_id
                           LIMIT 1
                           FOR UPDATE'
                        : 'SELECT id, nombre, activo, 0 AS cantidad
                           FROM producto_variantes
                           WHERE id = :variante_id
                             AND empresa_id = :empresa_id
                             AND producto_id = :producto_id
                           LIMIT 1
                           FOR UPDATE'
                );
            }

            $hasActiveVariants = false;
            if ($productVariantCountStmt !== null) {
                $productVariantCountStmt->execute([
                    ':empresa_id' => $companyId,
                    ':producto_id' => $productId,
                ]);
                $hasActiveVariants = (int) $productVariantCountStmt->fetchColumn() > 0;
            }

            if ($hasActiveVariants && $variantId <= 0) {
                throw new RuntimeException('Debes seleccionar una variación para ajustar este producto variable.');
            }

            if ($variantId > 0 && (!$hasVariantTable || !$variantSelectStmt)) {
                throw new RuntimeException('El módulo de variaciones no está disponible en la base de datos.');
            }

            $variant = null;
            if ($variantId > 0 && $variantSelectStmt !== null) {
                if (!$hasVariantQty) {
                    throw new RuntimeException(
                        'No puedes solicitar ajustes por variación sin columna cantidad en producto_variantes.'
                    );
                }

                if (!$this->isWholeQuantity($newStock)) {
                    throw new RuntimeException('El stock nuevo para variaciones debe ser un número entero.');
                }

                $variantSelectStmt->execute([
                    ':variante_id' => $variantId,
                    ':empresa_id' => $companyId,
                    ':producto_id' => $productId,
                ]);
                $variant = $variantSelectStmt->fetch();

                if (!$variant) {
                    throw new RuntimeException('La variación seleccionada no existe para el producto.');
                }

                if ((int) ($variant['activo'] ?? 0) !== 1) {
                    throw new RuntimeException('La variación seleccionada está inactiva.');
                }
            }

            $branchStockStmt = $pdo->prepare(
                'SELECT id, stock
                 FROM inventario
                 WHERE empresa_id = :empresa_id
                   AND sucursal_id = :sucursal_id
                   AND producto_id = :producto_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $branchStockStmt->execute([
                ':empresa_id' => $companyId,
                ':sucursal_id' => $branchId,
                ':producto_id' => $productId,
            ]);
            $branchStock = $branchStockStmt->fetch();

            if (!$branchStock) {
                $seedStmt = $pdo->prepare(
                    'INSERT INTO inventario (
                        empresa_id, sucursal_id, producto_id, stock, costo_promedio, updated_at
                    ) VALUES (
                        :empresa_id, :sucursal_id, :producto_id, 0, :costo_promedio, NOW()
                    )'
                );
                $seedStmt->execute([
                    ':empresa_id' => $companyId,
                    ':sucursal_id' => $branchId,
                    ':producto_id' => $productId,
                    ':costo_promedio' => (float) ($product['precio_compra'] ?? 0),
                ]);

                $branchStockStmt->execute([
                    ':empresa_id' => $companyId,
                    ':sucursal_id' => $branchId,
                    ':producto_id' => $productId,
                ]);
                $branchStock = $branchStockStmt->fetch();
            }

            $currentStock = $variantId > 0
                ? (float) ($variant['cantidad'] ?? 0)
                : (float) ($branchStock['stock'] ?? 0);

            if (abs($newStock - $currentStock) < 0.0001) {
                throw new RuntimeException('El stock nuevo no puede ser igual al stock actual.');
            }

            $adjustmentType = $newStock > $currentStock ? 'incremento' : 'decremento';

            $insertSql = $hasAdjustmentVariantColumn
                ? 'INSERT INTO ajustes_inventario (
                    empresa_id, sucursal_id, producto_id, variante_id, tipo_ajuste,
                    stock_anterior, stock_nuevo, motivo, usuario_id,
                    estado, created_at, updated_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :producto_id, :variante_id, :tipo_ajuste,
                    :stock_anterior, :stock_nuevo, :motivo, :usuario_id,
                    "pendiente", NOW(), NOW()
                )'
                : 'INSERT INTO ajustes_inventario (
                    empresa_id, sucursal_id, producto_id, tipo_ajuste,
                    stock_anterior, stock_nuevo, motivo, usuario_id,
                    estado, created_at, updated_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :producto_id, :tipo_ajuste,
                    :stock_anterior, :stock_nuevo, :motivo, :usuario_id,
                    "pendiente", NOW(), NOW()
                )';
            $insertStmt = $pdo->prepare($insertSql);

            $insertParams = [
                ':empresa_id' => $companyId,
                ':sucursal_id' => $branchId,
                ':producto_id' => $productId,
                ':tipo_ajuste' => $adjustmentType,
                ':stock_anterior' => $currentStock,
                ':stock_nuevo' => $newStock,
                ':motivo' => $reason,
                ':usuario_id' => $userId,
            ];
            if ($hasAdjustmentVariantColumn) {
                $insertParams[':variante_id'] = $variantId > 0 ? $variantId : null;
            }
            $insertStmt->execute($insertParams);

            return (int) $pdo->lastInsertId();
        });
    }

    public function approve(int $adjustmentId, string $observation = ''): int
    {
        $companyId = (int) (Tenant::companyId() ?? 0);
        $userId = (int) (Auth::id() ?? 0);

        if ($companyId <= 0 || $userId <= 0) {
            throw new RuntimeException('Sesión inválida para aprobar ajuste.');
        }

        if ($adjustmentId <= 0) {
            throw new RuntimeException('Ajuste inválido para aprobar.');
        }

        $approvalNote = trim($observation);

        return Database::transaction(function () use ($companyId, $userId, $adjustmentId, $approvalNote): int {
            $pdo = Database::connection();
            $hasAdjustmentVariantColumn = $this->tableHasColumn($pdo, 'ajustes_inventario', 'variante_id');
            $hasMovementVariantColumn = $this->tableHasColumn($pdo, 'movimientos_inventario', 'variante_id');
            $hasVariantTable = $this->hasVariantTable($pdo);
            $hasVariantQty = $hasVariantTable ? $this->hasVariantQuantityColumn($pdo) : false;

            $adjustmentStmt = $pdo->prepare(
                'SELECT *
                 FROM ajustes_inventario
                 WHERE id = :id
                   AND empresa_id = :empresa_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $adjustmentStmt->execute([
                ':id' => $adjustmentId,
                ':empresa_id' => $companyId,
            ]);
            $adjustment = $adjustmentStmt->fetch();

            if (!$adjustment) {
                throw new RuntimeException('El ajuste no existe en la empresa activa.');
            }

            if (($adjustment['estado'] ?? '') !== 'pendiente') {
                throw new RuntimeException('Solo se pueden aprobar ajustes en estado pendiente.');
            }

            $branchId = (int) ($adjustment['sucursal_id'] ?? 0);
            $productId = (int) ($adjustment['producto_id'] ?? 0);
            $variantId = $hasAdjustmentVariantColumn ? (int) ($adjustment['variante_id'] ?? 0) : 0;
            $targetStock = round((float) ($adjustment['stock_nuevo'] ?? 0), 2);

            if ($branchId <= 0 || $productId <= 0) {
                throw new RuntimeException('El ajuste no tiene sucursal o producto válido.');
            }

            if ($variantId > 0 && !$hasMovementVariantColumn) {
                throw new RuntimeException(
                    'No puedes aprobar ajustes por variación sin columna variante_id en movimientos_inventario. Ejecuta la migración 025.'
                );
            }

            if ($variantId > 0 && (!$hasVariantTable || !$hasVariantQty)) {
                throw new RuntimeException(
                    'No puedes aprobar ajustes por variación sin columna cantidad en producto_variantes.'
                );
            }

            if ($variantId > 0 && !$this->isWholeQuantity($targetStock)) {
                throw new RuntimeException('El stock objetivo de una variación debe ser un número entero.');
            }

            $inventoryStmt = $pdo->prepare(
                'SELECT id, stock
                 FROM inventario
                 WHERE empresa_id = :empresa_id
                   AND sucursal_id = :sucursal_id
                   AND producto_id = :producto_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $inventoryStmt->execute([
                ':empresa_id' => $companyId,
                ':sucursal_id' => $branchId,
                ':producto_id' => $productId,
            ]);
            $inventoryRow = $inventoryStmt->fetch();

            if (!$inventoryRow) {
                throw new RuntimeException('No existe inventario en la sucursal para aprobar este ajuste.');
            }

            $currentBranchStock = (float) ($inventoryRow['stock'] ?? 0);

            $productStmt = $pdo->prepare(
                'SELECT id, nombre, stock_actual, precio_compra
                 FROM productos
                 WHERE id = :producto_id
                   AND empresa_id = :empresa_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $productStmt->execute([
                ':producto_id' => $productId,
                ':empresa_id' => $companyId,
            ]);
            $product = $productStmt->fetch();

            if (!$product) {
                throw new RuntimeException('El producto asociado al ajuste no existe.');
            }

            $updateInventoryStmt = $pdo->prepare(
                'UPDATE inventario
                 SET stock = :stock,
                     updated_at = NOW()
                 WHERE id = :id'
            );

            $updateProductStmt = $pdo->prepare(
                'UPDATE productos
                 SET stock_actual = :stock_actual
                 WHERE id = :producto_id
                   AND empresa_id = :empresa_id'
            );

            $syncProductStockFromVariantsStmt = null;
            $variantSelectStmt = null;
            $variantUpdateStmt = null;
            if ($hasVariantTable && $hasVariantQty) {
                $syncProductStockFromVariantsStmt = $pdo->prepare(
                    'UPDATE productos p
                     SET p.stock_actual = (
                         SELECT COALESCE(SUM(v.cantidad), 0)
                         FROM producto_variantes v
                         WHERE v.empresa_id = p.empresa_id
                           AND v.producto_id = p.id
                           AND v.activo = 1
                     ),
                     p.updated_at = NOW()
                     WHERE p.id = :producto_id
                       AND p.empresa_id = :empresa_id'
                );

                $variantSelectStmt = $pdo->prepare(
                    'SELECT id, nombre, activo, cantidad
                     FROM producto_variantes
                     WHERE id = :variante_id
                       AND empresa_id = :empresa_id
                       AND producto_id = :producto_id
                     LIMIT 1
                     FOR UPDATE'
                );

                $variantUpdateStmt = $pdo->prepare(
                    'UPDATE producto_variantes
                     SET cantidad = :cantidad,
                         updated_at = NOW()
                     WHERE id = :variante_id
                       AND empresa_id = :empresa_id
                       AND producto_id = :producto_id'
                );
            }

            $delta = 0.0;
            if ($variantId > 0) {
                if (!$variantSelectStmt || !$variantUpdateStmt || !$syncProductStockFromVariantsStmt) {
                    throw new RuntimeException('No fue posible preparar actualización de stock por variación.');
                }

                $variantSelectStmt->execute([
                    ':variante_id' => $variantId,
                    ':empresa_id' => $companyId,
                    ':producto_id' => $productId,
                ]);
                $variant = $variantSelectStmt->fetch();

                if (!$variant) {
                    throw new RuntimeException('La variación asociada al ajuste no existe.');
                }

                if ((int) ($variant['activo'] ?? 0) !== 1) {
                    throw new RuntimeException('La variación asociada al ajuste está inactiva.');
                }

                $currentVariantStock = (float) ($variant['cantidad'] ?? 0);
                $delta = $targetStock - $currentVariantStock;

                $newBranchStock = $currentBranchStock + $delta;
                if ($newBranchStock < -0.0001) {
                    throw new RuntimeException('La aprobación dejaría el stock de sucursal en negativo.');
                }

                $updateInventoryStmt->execute([
                    ':stock' => max(0, $newBranchStock),
                    ':id' => (int) $inventoryRow['id'],
                ]);

                $variantUpdateStmt->execute([
                    ':cantidad' => (int) round($targetStock),
                    ':variante_id' => $variantId,
                    ':empresa_id' => $companyId,
                    ':producto_id' => $productId,
                ]);

                if ($variantUpdateStmt->rowCount() <= 0 && abs($delta) > 0.0001) {
                    throw new RuntimeException('No fue posible actualizar stock de la variación.');
                }

                $syncProductStockFromVariantsStmt->execute([
                    ':producto_id' => $productId,
                    ':empresa_id' => $companyId,
                ]);
            } else {
                $delta = $targetStock - $currentBranchStock;

                $globalStock = (float) ($product['stock_actual'] ?? 0);
                $newGlobalStock = $globalStock + $delta;
                if ($newGlobalStock < -0.0001) {
                    throw new RuntimeException('La aprobación dejaría el stock global en negativo.');
                }

                $updateInventoryStmt->execute([
                    ':stock' => $targetStock,
                    ':id' => (int) $inventoryRow['id'],
                ]);

                $updateProductStmt->execute([
                    ':stock_actual' => max(0, $newGlobalStock),
                    ':producto_id' => $productId,
                    ':empresa_id' => $companyId,
                ]);
            }

            $movementSql = $hasMovementVariantColumn
                ? 'INSERT INTO movimientos_inventario (
                    empresa_id, sucursal_id, producto_id, variante_id, tipo, referencia, cantidad, costo_unitario, created_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :producto_id, :variante_id, "ajuste", :referencia, :cantidad, :costo_unitario, NOW()
                )'
                : 'INSERT INTO movimientos_inventario (
                    empresa_id, sucursal_id, producto_id, tipo, referencia, cantidad, costo_unitario, created_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :producto_id, "ajuste", :referencia, :cantidad, :costo_unitario, NOW()
                )';
            $movementStmt = $pdo->prepare($movementSql);

            $movementParams = [
                ':empresa_id' => $companyId,
                ':sucursal_id' => $branchId,
                ':producto_id' => $productId,
                ':referencia' => 'AJUSTE#' . $adjustmentId,
                ':cantidad' => $delta,
                ':costo_unitario' => (float) ($product['precio_compra'] ?? 0),
            ];
            if ($hasMovementVariantColumn) {
                $movementParams[':variante_id'] = $variantId > 0 ? $variantId : null;
            }
            $movementStmt->execute($movementParams);

            $updateAdjustmentStmt = $pdo->prepare(
                'UPDATE ajustes_inventario
                 SET estado = "aprobado",
                     aprobado_por_id = :aprobado_por_id,
                     observacion_aprobacion = :observacion_aprobacion,
                     aprobado_at = NOW(),
                     updated_at = NOW()
                 WHERE id = :id
                   AND empresa_id = :empresa_id'
            );
            $updateAdjustmentStmt->execute([
                ':aprobado_por_id' => $userId,
                ':observacion_aprobacion' => $approvalNote !== '' ? $approvalNote : null,
                ':id' => $adjustmentId,
                ':empresa_id' => $companyId,
            ]);

            return $adjustmentId;
        });
    }

    public function reject(int $adjustmentId, string $observation): int
    {
        $companyId = (int) (Tenant::companyId() ?? 0);
        $userId = (int) (Auth::id() ?? 0);

        if ($companyId <= 0 || $userId <= 0) {
            throw new RuntimeException('Sesión inválida para rechazar ajuste.');
        }

        if ($adjustmentId <= 0) {
            throw new RuntimeException('Ajuste inválido para rechazar.');
        }

        $normalizedObservation = trim($observation);
        if ($normalizedObservation === '') {
            throw new RuntimeException('La observación es obligatoria para rechazar el ajuste.');
        }

        return Database::transaction(function () use ($companyId, $userId, $adjustmentId, $normalizedObservation): int {
            $pdo = Database::connection();

            $stmt = $pdo->prepare(
                'UPDATE ajustes_inventario
                 SET estado = "rechazado",
                     aprobado_por_id = :aprobado_por_id,
                     observacion_aprobacion = :observacion_aprobacion,
                     aprobado_at = NOW(),
                     updated_at = NOW()
                 WHERE id = :id
                   AND empresa_id = :empresa_id
                   AND estado = "pendiente"'
            );
            $stmt->execute([
                ':aprobado_por_id' => $userId,
                ':observacion_aprobacion' => $normalizedObservation,
                ':id' => $adjustmentId,
                ':empresa_id' => $companyId,
            ]);

            if ($stmt->rowCount() <= 0) {
                throw new RuntimeException('El ajuste no está pendiente o no existe.');
            }

            return $adjustmentId;
        });
    }

    private function isWholeQuantity(float $quantity): bool
    {
        return abs($quantity - round($quantity)) < 0.0001;
    }

    private function hasVariantTable(\PDO $pdo): bool
    {
        if ($this->variantTableAvailable !== null) {
            return $this->variantTableAvailable;
        }

        $stmt = $pdo->prepare(
            "SELECT COUNT(*) FROM information_schema.TABLES
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = 'producto_variantes'"
        );
        $stmt->execute();

        $this->variantTableAvailable = (int) $stmt->fetchColumn() > 0;
        return $this->variantTableAvailable;
    }

    private function hasVariantQuantityColumn(\PDO $pdo): bool
    {
        if ($this->variantQuantityColumnAvailable !== null) {
            return $this->variantQuantityColumnAvailable;
        }

        $stmt = $pdo->prepare(
            "SELECT COUNT(*) FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = 'producto_variantes'
               AND COLUMN_NAME = 'cantidad'"
        );
        $stmt->execute();

        $this->variantQuantityColumnAvailable = (int) $stmt->fetchColumn() > 0;
        return $this->variantQuantityColumnAvailable;
    }

    private function tableHasColumn(\PDO $pdo, string $table, string $column): bool
    {
        $cacheKey = $table . '.' . $column;
        if (array_key_exists($cacheKey, $this->columnAvailabilityCache)) {
            return $this->columnAvailabilityCache[$cacheKey];
        }

        $stmt = $pdo->prepare(
            "SELECT COUNT(*) FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = :table
               AND COLUMN_NAME = :column"
        );
        $stmt->execute([
            ':table' => $table,
            ':column' => $column,
        ]);

        $this->columnAvailabilityCache[$cacheKey] = (int) $stmt->fetchColumn() > 0;
        return $this->columnAvailabilityCache[$cacheKey];
    }
}
