<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\InventoryAdjustmentRepositoryInterface;
use ApiModel\Domain\Repository\InventoryRepositoryInterface;
use ApiModel\Domain\Repository\JournalRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\NotFoundException;
use PDO;

final class InventoryAdjustmentService
{
    public function __construct(
        private readonly InventoryAdjustmentRepositoryInterface $repository,
        private readonly InventoryRepositoryInterface $inventory,
        private readonly JournalRepositoryInterface $journals,
        private readonly JournalService $journalService,
        private readonly PDO $pdo
    ) {}

    public function listAdjustments(string $companyId): array
    {
        return $this->repository->listByCompany($companyId);
    }

    public function getAdjustment(string $companyId, string $id): array
    {
        $adjustment = $this->repository->findById($id);
        if ($adjustment === null || (string) $adjustment['company_id'] !== $companyId) {
            throw new NotFoundException('Ajuste de inventario no encontrado');
        }

        return [
            ...$adjustment,
            'lines' => $this->repository->findLinesByAdjustmentId($id),
        ];
    }

    public function saveAdjustment(string $companyId, ?string $branchId, ?string $userId, array $data): array
    {
        $id = trim((string) ($data['id'] ?? ''));
        $isUpdate = $id !== '' && $id !== '0';

        $this->pdo->beginTransaction();

        try {
            if ($isUpdate) {
                // For adjustments, we typically delete the old physical stock changes and recreate.
                $existing = $this->repository->findById($id);
                if ($existing === null || (string) $existing['company_id'] !== $companyId) {
                    throw new NotFoundException('Ajuste de inventario no encontrado');
                }
                $adjustmentNumber = (string) $existing['adjustment_number'];

                // Revert stock changes of the old lines
                $oldLines = $this->repository->findLinesByAdjustmentId($id);
                foreach ($oldLines as $oldLine) {
                    $currStock = $this->inventory->getStock((string) $oldLine['item_id'], (string) $oldLine['warehouse_id']);
                    $this->inventory->updateStock((string) $oldLine['item_id'], (string) $oldLine['warehouse_id'], $currStock - (float) $oldLine['quantity']);
                }

                $this->repository->deleteLines($id);
            } else {
                $id = Database::uuid();
                $adjustmentNumber = $this->repository->nextAdjustmentNumber($companyId);
            }

            $adjustmentDate = trim((string) ($data['adjustment_date'] ?? date('Y-m-d')));
            $notes = isset($data['notes']) ? trim((string) $data['notes']) : '';
            $lines = $data['lines'] ?? [];

            if ($adjustmentDate === '') {
                throw new BadRequestException('La fecha de ajuste es obligatoria');
            }
            if (count($lines) === 0) {
                throw new BadRequestException('El ajuste de inventario debe tener al menos una línea de detalle');
            }

            $currencies = $this->journalService->resolveCurrencies($companyId, $branchId, $adjustmentDate);
            $currencyCode = $currencies['operating_currency'];
            $exchangeRate = (float) $currencies['exchange_rate'];

            $processedLines = [];

            foreach ($lines as $line) {
                $itemId = trim((string) ($line['item_id'] ?? ''));
                $warehouseId = trim((string) ($line['warehouse_id'] ?? ''));
                $quantity = (float) ($line['quantity'] ?? 0.0);
                $unitCost = (float) ($line['unit_cost'] ?? 0.0);
                $chartAccountId = trim((string) ($line['chart_account_id'] ?? ''));

                if ($itemId === '') {
                    throw new BadRequestException('El producto es obligatorio en cada línea');
                }
                if ($warehouseId === '') {
                    throw new BadRequestException('La bodega es obligatoria en cada línea');
                }
                if ($quantity === 0.0) {
                    throw new BadRequestException('La cantidad no puede ser cero');
                }
                if ($unitCost < 0.0) {
                    throw new BadRequestException('El costo unitario no puede ser negativo');
                }
                if ($chartAccountId === '') {
                    throw new BadRequestException('La cuenta de contrapartida es obligatoria');
                }

                // Check item exists
                $item = $this->inventory->findItemById($itemId);
                if ($item === null || (string) $item['company_id'] !== $companyId) {
                    throw new NotFoundException('Producto no encontrado');
                }

                if ((string) $item['item_type'] !== 'product') {
                    throw new BadRequestException('Solo se pueden hacer ajustes de inventario a productos físicos (no servicios)');
                }

                // Apply physical stock changes
                $currStock = $this->inventory->getStock($itemId, $warehouseId);
                $newStock = $currStock + $quantity;
                
                // Allow negative stock in system if needed, but we check if it is valid.
                $this->inventory->updateStock($itemId, $warehouseId, $newStock);

                $processedLines[] = [
                    'id' => Database::uuid(),
                    'item_id' => $itemId,
                    'warehouse_id' => $warehouseId,
                    'quantity' => $quantity,
                    'unit_cost' => $unitCost,
                    'chart_account_id' => $chartAccountId,
                    'inventory_account_id' => $item['inventory_account_id']
                ];
            }

            $adjustmentObj = [
                'id' => $id,
                'company_id' => $companyId,
                'branch_id' => $branchId,
                'adjustment_number' => $adjustmentNumber,
                'adjustment_date' => $adjustmentDate,
                'notes' => $notes,
                'created_by' => $userId,
            ];

            if ($isUpdate) {
                $this->repository->delete($id);
            }
            $this->repository->insert($adjustmentObj);
            $this->repository->insertLines($id, $processedLines);

            // Accounting Auto-Posting
            $this->deleteJournalEntryForAdjustment($companyId, $adjustmentNumber);
            $this->postAdjustmentToAccounting($companyId, $branchId, $userId, $adjustmentObj, $processedLines, $currencyCode, $exchangeRate);

            $this->pdo->commit();

            return $this->getAdjustment($companyId, $id);
        } catch (\Exception $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }
    }

    public function deleteAdjustment(string $companyId, string $id): void
    {
        $adjustment = $this->repository->findById($id);
        if ($adjustment === null || (string) $adjustment['company_id'] !== $companyId) {
            throw new NotFoundException('Ajuste de inventario no encontrado');
        }

        $this->pdo->beginTransaction();

        try {
            $lines = $this->repository->findLinesByAdjustmentId($id);

            // Revert stock changes
            foreach ($lines as $line) {
                $currStock = $this->inventory->getStock((string) $line['item_id'], (string) $line['warehouse_id']);
                $this->inventory->updateStock((string) $line['item_id'], (string) $line['warehouse_id'], $currStock - (float) $line['quantity']);
            }

            $this->deleteJournalEntryForAdjustment($companyId, (string) $adjustment['adjustment_number']);
            $this->repository->deleteLines($id);
            $this->repository->delete($id);

            $this->pdo->commit();
        } catch (\Exception $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }
    }

    private function postAdjustmentToAccounting(
        string $companyId,
        ?string $branchId,
        ?string $userId,
        array $adjustment,
        array $lines,
        string $currencyCode,
        float $exchangeRate
    ): void {
        $jeId = Database::uuid();
        $journalLines = [];
        $order = 0;

        foreach ($lines as $line) {
            $qty = (float) $line['quantity'];
            $cost = (float) $line['unit_cost'];
            $totalAmount = round(abs($qty) * $cost, 4);

            if ($totalAmount <= 0) {
                continue;
            }

            $inventoryAccount = $line['inventory_account_id'];
            $adjustmentAccount = $line['chart_account_id'];

            if (empty($inventoryAccount)) {
                throw new BadRequestException('El producto no tiene una cuenta de inventario configurada.');
            }

            if ($qty > 0) {
                // Stock Increment
                // Debit: Asset Inventory Account
                $journalLines[] = [
                    'chart_account_id' => $inventoryAccount,
                    'line_order' => $order++,
                    'description' => "Incremento Inventario - Ajuste #{$adjustment['adjustment_number']}",
                    'debit' => $totalAmount,
                    'credit' => 0.0,
                    'debit_functional' => round($totalAmount * $exchangeRate, 4),
                    'credit_functional' => 0.0,
                ];
                // Credit: Income / Counterpart Account
                $journalLines[] = [
                    'chart_account_id' => $adjustmentAccount,
                    'line_order' => $order++,
                    'description' => "Contrapartida Ajuste - Ajuste #{$adjustment['adjustment_number']}",
                    'debit' => 0.0,
                    'credit' => $totalAmount,
                    'debit_functional' => 0.0,
                    'credit_functional' => round($totalAmount * $exchangeRate, 4),
                ];
            } else {
                // Stock Decrement
                // Debit: Expense / Counterpart Account
                $journalLines[] = [
                    'chart_account_id' => $adjustmentAccount,
                    'line_order' => $order++,
                    'description' => "Contrapartida Ajuste - Ajuste #{$adjustment['adjustment_number']}",
                    'debit' => $totalAmount,
                    'credit' => 0.0,
                    'debit_functional' => round($totalAmount * $exchangeRate, 4),
                    'credit_functional' => 0.0,
                ];
                // Credit: Asset Inventory Account
                $journalLines[] = [
                    'chart_account_id' => $inventoryAccount,
                    'line_order' => $order++,
                    'description' => "Disminución Inventario - Ajuste #{$adjustment['adjustment_number']}",
                    'debit' => 0.0,
                    'credit' => $totalAmount,
                    'debit_functional' => 0.0,
                    'credit_functional' => round($totalAmount * $exchangeRate, 4),
                ];
            }
        }

        if (count($journalLines) > 0) {
            $this->journals->insertEntry(
                $jeId,
                $companyId,
                $branchId,
                (string) $adjustment['adjustment_number'],
                (string) $adjustment['adjustment_date'],
                "Asiento contable automático - Ajuste de Inventario #{$adjustment['adjustment_number']}",
                'posted',
                $currencyCode,
                $currencyCode,
                (string) $exchangeRate,
                $userId
            );

            $this->journals->insertLines($jeId, $journalLines);
        }
    }

    private function deleteJournalEntryForAdjustment(string $companyId, string $adjustmentNumber): void
    {
        $st = $this->pdo->prepare('SELECT id FROM journal_entries WHERE company_id = ? AND entry_number = ? LIMIT 1');
        $st->execute([$companyId, $adjustmentNumber]);
        $jeId = $st->fetchColumn();

        if ($jeId !== false) {
            $this->journals->deleteLinesByEntry((string) $jeId);
            $this->journals->deleteEntry((string) $jeId);
        }
    }
}
