<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\BudgetRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\NotFoundException;
use PDO;

final class BudgetService
{
    public function __construct(
        private readonly BudgetRepositoryInterface $budget,
        private readonly PDO $pdo
    ) {}

    // ==========================================
    // AÑOS FISCALES
    // ==========================================

    /** @return list<array<string, mixed>> */
    public function listFiscalYears(): array
    {
        return $this->budget->listFiscalYears();
    }

    /** @param array<string, mixed> $data */
    public function createFiscalYear(array $data): array
    {
        $year = (int) ($data['year'] ?? 0);
        if ($year < 2000 || $year > 2100) {
            throw new BadRequestException('El año fiscal debe ser un año válido (ej. 2026)');
        }

        $existing = $this->budget->findFiscalYearByYear($year);
        if ($existing !== null) {
            throw new BadRequestException("El año fiscal {$year} ya existe");
        }

        $id = Database::uuid();
        $status = trim((string) ($data['status'] ?? 'draft'));
        if (!in_array($status, ['draft', 'open', 'closed'], true)) {
            $status = 'draft';
        }

        $this->budget->insertFiscalYear($id, $year, $status);
        return $this->budget->findFiscalYearById($id);
    }

    public function updateFiscalYearStatus(string $id, string $status): array
    {
        $existing = $this->budget->findFiscalYearById($id);
        if ($existing === null) {
            throw new NotFoundException('Año fiscal no encontrado');
        }

        if (!in_array($status, ['draft', 'open', 'closed'], true)) {
            throw new BadRequestException('Estado de año fiscal no válido. Permitidos: draft, open, closed');
        }

        $this->budget->updateFiscalYearStatus($id, $status);
        return $this->budget->findFiscalYearById($id);
    }

    // ==========================================
    // CLASIFICADORES
    // ==========================================

    /** @return list<array<string, mixed>> */
    public function listClassifiers(): array
    {
        return $this->budget->listClassifiers();
    }

    /** @param array<string, mixed> $data */
    public function createClassifier(array $data): array
    {
        $code = trim((string) ($data['code'] ?? ''));
        $name = trim((string) ($data['name'] ?? ''));
        $type = trim((string) ($data['type'] ?? ''));
        $parentId = isset($data['parent_id']) && trim((string) $data['parent_id']) !== '' ? trim((string) $data['parent_id']) : null;
        $accountId = isset($data['account_id']) && trim((string) $data['account_id']) !== '' ? trim((string) $data['account_id']) : null;

        if ($code === '') {
            throw new BadRequestException('El código del clasificador es obligatorio');
        }
        if ($name === '') {
            throw new BadRequestException('El nombre del clasificador es obligatorio');
        }
        if (!in_array($type, ['income', 'expense'], true)) {
            throw new BadRequestException('El tipo de clasificador debe ser "income" o "expense"');
        }

        $existing = $this->budget->findClassifierByCode($code);
        if ($existing !== null) {
            throw new BadRequestException("El clasificador con código '{$code}' ya existe");
        }

        if ($parentId !== null) {
            $parent = $this->budget->findClassifierById($parentId);
            if ($parent === null) {
                throw new BadRequestException('El clasificador padre especificado no existe');
            }
        }

        $id = Database::uuid();
        $this->budget->insertClassifier($id, $code, $name, $type, $parentId, $accountId);

        return $this->budget->findClassifierById($id);
    }

    /** @param array<string, mixed> $data */
    public function updateClassifier(string $id, array $data): array
    {
        $existing = $this->budget->findClassifierById($id);
        if ($existing === null) {
            throw new NotFoundException('Clasificador no encontrado');
        }

        $name = trim((string) ($data['name'] ?? $existing['name']));
        if ($name === '') {
            throw new BadRequestException('El nombre del clasificador es obligatorio');
        }
        $isActive = (bool) ($data['is_active'] ?? $existing['is_active']);
        $accountId = array_key_exists('account_id', $data) ? (trim((string) $data['account_id']) ?: null) : ($existing['account_id'] ?: null);

        $this->budget->updateClassifier($id, $name, $isActive, $accountId);
        return $this->budget->findClassifierById($id);
    }

    // ==========================================
    // UNIDADES ORGANIZACIONALES
    // ==========================================

    /** @return list<array<string, mixed>> */
    public function listOrgUnits(): array
    {
        return $this->budget->listOrgUnits();
    }

    /** @param array<string, mixed> $data */
    public function createOrgUnit(array $data): array
    {
        $code = trim((string) ($data['code'] ?? ''));
        $name = trim((string) ($data['name'] ?? ''));
        $parentId = isset($data['parent_id']) && trim((string) $data['parent_id']) !== '' ? trim((string) $data['parent_id']) : null;

        if ($code === '') {
            throw new BadRequestException('El código de la unidad es obligatorio');
        }
        if ($name === '') {
            throw new BadRequestException('El nombre de la unidad es obligatorio');
        }

        $existing = $this->budget->findOrgUnitByCode($code);
        if ($existing !== null) {
            throw new BadRequestException("La unidad con código '{$code}' ya existe");
        }

        if ($parentId !== null) {
            $parent = $this->budget->findOrgUnitById($parentId);
            if ($parent === null) {
                throw new BadRequestException('La unidad organizativa padre especificada no existe');
            }
        }

        $id = Database::uuid();
        $this->budget->insertOrgUnit($id, $code, $name, $parentId);
        return $this->budget->findOrgUnitById($id);
    }

    /** @param array<string, mixed> $data */
    public function updateOrgUnit(string $id, array $data): array
    {
        $existing = $this->budget->findOrgUnitById($id);
        if ($existing === null) {
            throw new NotFoundException('Unidad organizativa no encontrada');
        }

        $name = trim((string) ($data['name'] ?? $existing['name']));
        if ($name === '') {
            throw new BadRequestException('El nombre de la unidad es obligatorio');
        }
        $isActive = (bool) ($data['is_active'] ?? $existing['is_active']);

        $this->budget->updateOrgUnit($id, $name, $isActive);
        return $this->budget->findOrgUnitById($id);
    }

    // ==========================================
    // FORMULACIÓN (PIA / PIM)
    // ==========================================

    /** @return list<array<string, mixed>> */
    public function listFormulations(string $fiscalYearId, ?string $orgUnitId): array
    {
        return $this->budget->listFormulations($fiscalYearId, $orgUnitId);
    }

    /** @param array<string, mixed> $data */
    public function saveFormulation(string $fiscalYearId, array $data): void
    {
        $fy = $this->budget->findFiscalYearById($fiscalYearId);
        if ($fy === null) {
            throw new NotFoundException('Año fiscal no encontrado');
        }

        $orgUnitId = trim((string) ($data['org_unit_id'] ?? ''));
        $classifierId = trim((string) ($data['classifier_id'] ?? ''));
        $requestedAmount = (float) ($data['requested_amount'] ?? 0.0);
        $approvedAmount = (float) ($data['approved_amount'] ?? 0.0);
        $currentAmount = (float) ($data['current_amount'] ?? $approvedAmount);
        $status = trim((string) ($data['status'] ?? 'draft'));

        if ($orgUnitId === '' || $classifierId === '') {
            throw new BadRequestException('La unidad organizativa y el clasificador son obligatorios');
        }

        $org = $this->budget->findOrgUnitById($orgUnitId);
        if ($org === null) {
            throw new BadRequestException('La unidad organizativa no existe');
        }

        $classifier = $this->budget->findClassifierById($classifierId);
        if ($classifier === null) {
            throw new BadRequestException('El clasificador presupuestario no existe');
        }

        if ($requestedAmount < 0 || $approvedAmount < 0 || $currentAmount < 0) {
            throw new BadRequestException('Los montos del presupuesto no pueden ser negativos');
        }

        $id = Database::uuid();
        $this->budget->saveFormulation($id, $fiscalYearId, $orgUnitId, $classifierId, $requestedAmount, $approvedAmount, $currentAmount, $status);
    }

    // ==========================================
    // MODIFICACIONES PRESUPUESTARIAS
    // ==========================================

    /** @return list<array<string, mixed>> */
    public function listModifications(string $fiscalYearId): array
    {
        return $this->budget->listModifications($fiscalYearId);
    }

    /** @return array<string, mixed> */
    public function getModification(string $id): array
    {
        $mod = $this->budget->findModificationById($id);
        if ($mod === null) {
            throw new NotFoundException('Modificación presupuestaria no encontrada');
        }
        $mod['entries'] = $this->budget->getModificationEntries($id);
        return $mod;
    }

    /** @param array<string, mixed> $data */
    public function createModification(string $userId, array $data): array
    {
        $fiscalYearId = trim((string) ($data['fiscal_year_id'] ?? ''));
        $type = trim((string) ($data['type'] ?? ''));
        $docNumber = trim((string) ($data['document_number'] ?? ''));
        $description = isset($data['description']) ? trim((string) $data['description']) : null;
        $approvedAt = trim((string) ($data['approved_at'] ?? ''));
        $status = trim((string) ($data['status'] ?? 'draft'));
        $entries = $data['entries'] ?? [];

        if ($fiscalYearId === '' || $type === '' || $docNumber === '' || $approvedAt === '') {
            throw new BadRequestException('El año fiscal, tipo, número de documento y fecha son obligatorios');
        }

        if (!in_array($type, ['transfer', 'increase', 'decrease'], true)) {
            throw new BadRequestException('Tipo de modificación no válido');
        }

        if (empty($entries)) {
            throw new BadRequestException('La modificación debe contener al menos un registro de detalle');
        }

        // Si es traspaso, validar que esté balanceada
        if ($type === 'transfer') {
            $sum = 0.0;
            foreach ($entries as $entry) {
                $sum += (float) ($entry['amount'] ?? 0.0);
            }
            if (abs($sum) > 0.0001) {
                throw new BadRequestException('El traspaso debe estar balanceado (la suma de orígenes y destinos debe ser igual a cero)');
            }
        }

        $id = Database::uuid();

        $this->pdo->beginTransaction();
        try {
            $this->budget->insertModification($id, $fiscalYearId, $type, $docNumber, $description, $approvedAt, $status, $userId);

            foreach ($entries as $entry) {
                $orgUnitId = trim((string) ($entry['org_unit_id'] ?? ''));
                $classifierId = trim((string) ($entry['classifier_id'] ?? ''));
                $amount = (float) ($entry['amount'] ?? 0.0);

                if ($orgUnitId === '' || $classifierId === '') {
                    throw new BadRequestException('Líneas de detalle incompletas');
                }

                $entryId = Database::uuid();
                $this->budget->insertModificationEntry($entryId, $id, $orgUnitId, $classifierId, $amount);

                // Si está aprobada, actualizamos el PIM (current_amount) directamente
                if ($status === 'approved') {
                    $formulation = $this->budget->findFormulation($fiscalYearId, $orgUnitId, $classifierId);
                    if ($formulation === null) {
                        // Creamos una formulación vacía inicial para poder actualizarla
                        $formulationId = Database::uuid();
                        $this->budget->saveFormulation($formulationId, $fiscalYearId, $orgUnitId, $classifierId, 0.0, 0.0, 0.0, 'approved');
                    }
                    
                    // Si es decremento o transferencia de salida (monto negativo), verificar disponibilidad previa
                    if ($amount < 0 && $type !== 'increase') {
                        $availInfo = $this->budget->getAvailability($fiscalYearId, $orgUnitId, $classifierId);
                        if ($availInfo['available'] < abs($amount)) {
                            throw new BadRequestException("Saldo presupuestario insuficiente para realizar la reducción/traspaso de fondos.");
                        }
                    }

                    $this->budget->updateFormulationCurrentAmount($fiscalYearId, $orgUnitId, $classifierId, $amount);
                }
            }

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

        return $this->getModification($id);
    }

    // ==========================================
    // DISPONIBILIDAD Y EJECUCIÓN
    // ==========================================

    /** @return array<string, mixed> */
    public function getAvailabilityInfo(string $fiscalYearId, string $orgUnitId, string $classifierId): array
    {
        return $this->budget->getAvailability($fiscalYearId, $orgUnitId, $classifierId);
    }

    public function verifyAvailability(string $fiscalYearId, string $orgUnitId, string $classifierId, float $amount): void
    {
        $classifier = $this->budget->findClassifierById($classifierId);
        if ($classifier === null) {
            throw new NotFoundException('Clasificador presupuestario no encontrado');
        }

        // La validación de disponibilidad solo aplica para Egresos (Gastos)
        if ($classifier['type'] !== 'expense') {
            return;
        }

        $info = $this->budget->getAvailability($fiscalYearId, $orgUnitId, $classifierId);
        $available = $info['available'];

        if ($available < $amount) {
            throw new BadRequestException(sprintf(
                'Saldo presupuestario insuficiente para la partida %s. Solicitado: %s, Disponible: %s',
                $classifier['code'],
                number_format($amount, 2),
                number_format($available, 2)
            ));
        }
    }

    /** @param array<string, mixed> $data */
    public function recordExecution(string $userId, array $data): void
    {
        $fiscalYearId = trim((string) ($data['fiscal_year_id'] ?? ''));
        $orgUnitId = trim((string) ($data['org_unit_id'] ?? ''));
        $classifierId = trim((string) ($data['classifier_id'] ?? ''));
        $phase = trim((string) ($data['phase'] ?? ''));
        $docType = trim((string) ($data['source_document_type'] ?? ''));
        $docId = trim((string) ($data['source_document_id'] ?? ''));
        $refExecId = isset($data['ref_execution_id']) && trim((string) $data['ref_execution_id']) !== '' ? trim((string) $data['ref_execution_id']) : null;
        $amount = (float) ($data['amount'] ?? 0.0);
        $description = isset($data['description']) ? trim((string) $data['description']) : null;

        if ($fiscalYearId === '' || $orgUnitId === '' || $classifierId === '' || $phase === '' || $docType === '' || $docId === '') {
            throw new BadRequestException('Faltan campos obligatorios para registrar la ejecución');
        }

        if (!in_array($phase, ['pre_commitment', 'commitment', 'accrued', 'paid'], true)) {
            throw new BadRequestException('Fase de ejecución presupuestaria no válida');
        }

        // Si es fase preventiva (precompromiso o compromiso directo), verificar saldo disponible
        if ($phase === 'pre_commitment' || ($phase === 'commitment' && $refExecId === null)) {
            $this->verifyAvailability($fiscalYearId, $orgUnitId, $classifierId, $amount);
        }

        $id = Database::uuid();
        $this->budget->insertExecution(
            $id,
            $fiscalYearId,
            $orgUnitId,
            $classifierId,
            $phase,
            $docType,
            $docId,
            $refExecId,
            $amount,
            $description,
            $userId
        );
    }

    /** @return list<array<string, mixed>> */
    public function listExecutions(string $fiscalYearId, ?string $orgUnitId, ?string $classifierId, ?string $startDate = null, ?string $endDate = null): array
    {
        return $this->budget->listExecutions($fiscalYearId, $orgUnitId, $classifierId, $startDate, $endDate);
    }

    /** @return list<array<string, mixed>> */
    public function getExecutionSummary(string $fiscalYearId, ?string $orgUnitId, ?string $startDate = null, ?string $endDate = null): array
    {
        return $this->budget->getExecutionSummary($fiscalYearId, $orgUnitId, $startDate, $endDate);
    }

    /** @return array<string, mixed>|null */
    public function findFiscalYearByYear(int $year): ?array
    {
        return $this->budget->findFiscalYearByYear($year);
    }

    public function deleteExecutionsBySource(string $sourceDocumentType, string $sourceDocumentId): void
    {
        $this->budget->deleteExecutionsBySource($sourceDocumentType, $sourceDocumentId);
    }
}
