<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\BranchRepositoryInterface;
use ApiModel\Domain\Repository\ChartAccountRepositoryInterface;
use ApiModel\Domain\Repository\CompanyRepositoryInterface;
use ApiModel\Domain\Repository\ExchangeRateRepositoryInterface;
use ApiModel\Domain\Repository\JournalRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\NotFoundException;

final class JournalService
{
    public function __construct(
        private readonly JournalRepositoryInterface $journals,
        private readonly ChartAccountRepositoryInterface $accounts,
        private readonly CompanyRepositoryInterface $companies,
        private readonly BranchRepositoryInterface $branches,
        private readonly ExchangeRateRepositoryInterface $exchangeRates
    ) {}

    /** @return list<array<string, mixed>> */
    public function listEntries(string $companyId, ?string $branchId = null): array
    {
        return array_map(fn (array $row): array => $this->mapEntrySummary($row), $this->journals->listEntriesByCompany($companyId, $branchId));
    }

    /** @return array<string, mixed> */
    public function getEntry(string $companyId, string $id): array
    {
        $entry = $this->journals->findEntryById($id);
        if ($entry === null || (string) $entry['company_id'] !== $companyId) {
            throw new NotFoundException('Asiento no encontrado');
        }

        return $this->mapEntryDetail($entry);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function saveEntry(string $companyId, ?string $branchId, ?string $userId, array $data): array
    {
        $entryId = trim((string) ($data['id'] ?? ''));
        $isUpdate = $entryId !== '';

        if ($isUpdate) {
            $existing = $this->journals->findEntryById($entryId);
            if ($existing === null || (string) $existing['company_id'] !== $companyId) {
                throw new NotFoundException('Asiento no encontrado');
            }
            if ((string) $existing['status'] !== 'draft') {
                throw new BadRequestException('Solo se pueden editar asientos en borrador');
            }
        }

        $entryDate = trim((string) ($data['entry_date'] ?? date('Y-m-d')));
        $description = isset($data['description']) ? trim((string) $data['description']) : null;
        $lines = $data['lines'] ?? [];
        if (! is_array($lines) || count($lines) < 2) {
            throw new BadRequestException('El asiento requiere al menos 2 líneas');
        }

        $currencies = $this->resolveCurrencies($companyId, $branchId, $entryDate);
        $operating = $currencies['operating_currency'];
        $functional = $currencies['functional_currency'];
        $rate = $currencies['exchange_rate'];

        $normalizedLines = $this->normalizeLines($companyId, $lines, $rate);
        $this->assertBalanced($normalizedLines);

        if ($isUpdate) {
            $this->journals->deleteLinesByEntry($entryId);
            $this->journals->updateEntry(
                $entryId,
                $branchId,
                $entryDate,
                $description,
                $operating,
                $functional,
                $rate
            );
            $this->journals->insertLines($entryId, $normalizedLines);

            return $this->getEntry($companyId, $entryId);
        }

        $newId = Database::uuid();
        $entryNumber = $this->journals->nextEntryNumber($companyId);
        $this->journals->insertEntry(
            $newId,
            $companyId,
            $branchId,
            $entryNumber,
            $entryDate,
            $description,
            'draft',
            $operating,
            $functional,
            $rate,
            $userId
        );
        $this->journals->insertLines($newId, $normalizedLines);

        return $this->getEntry($companyId, $newId);
    }

    /** @return array<string, mixed> */
    public function postEntry(string $companyId, string $id): array
    {
        $entry = $this->journals->findEntryById($id);
        if ($entry === null || (string) $entry['company_id'] !== $companyId) {
            throw new NotFoundException('Asiento no encontrado');
        }
        if ((string) $entry['status'] === 'posted') {
            throw new BadRequestException('El asiento ya está contabilizado');
        }

        $lines = $this->journals->listLinesByEntry($id);
        if (count($lines) < 2) {
            throw new BadRequestException('El asiento no tiene líneas suficientes');
        }

        $this->journals->updateEntryStatus($id, 'posted');

        return $this->getEntry($companyId, $id);
    }

    /** @return array{operating_currency: string, functional_currency: string, exchange_rate: string} */
    public function resolveCurrencies(string $companyId, ?string $branchId, string $entryDate): array
    {
        $company = $this->companies->findById($companyId);
        if ($company === null) {
            throw new NotFoundException('Empresa no encontrada');
        }

        $functional = strtoupper(trim((string) ($company['base_currency'] ?? 'USD')));
        $operating = $functional;

        if ($branchId !== null && $branchId !== '') {
            $branch = $this->branches->findById($branchId);
            if ($branch !== null && (string) $branch['company_id'] === $companyId) {
                $operating = strtoupper(trim((string) ($branch['operating_currency'] ?? $functional)));
            }
        }

        if ($operating === $functional) {
            return [
                'operating_currency' => $operating,
                'functional_currency' => $functional,
                'exchange_rate' => '1',
            ];
        }

        $rateRow = $this->exchangeRates->findRate($companyId, $entryDate, $operating, $functional);
        if ($rateRow === null) {
            throw new BadRequestException(
                "No hay tipo de cambio {$operating}→{$functional} para la fecha {$entryDate}"
            );
        }

        return [
            'operating_currency' => $operating,
            'functional_currency' => $functional,
            'exchange_rate' => (string) $rateRow['rate'],
        ];
    }

    /** @param list<array<string, mixed>> $lines @return list<array<string, mixed>> */
    private function normalizeLines(string $companyId, array $lines, string $rate): array
    {
        $out = [];
        $rateNum = (float) $rate;

        foreach ($lines as $index => $line) {
            $accountId = trim((string) ($line['chart_account_id'] ?? ''));
            if ($accountId === '') {
                throw new BadRequestException('Cada línea requiere una cuenta');
            }

            $account = $this->accounts->findById($accountId);
            if ($account === null || (string) $account['company_id'] !== $companyId) {
                throw new BadRequestException('Cuenta contable inválida en una línea');
            }
            if ((bool) ($account['is_header'] ?? false)) {
                throw new BadRequestException('No se puede usar una cuenta grupo en asientos: ' . $account['code']);
            }

            $debit = round((float) ($line['debit'] ?? 0), 4);
            $credit = round((float) ($line['credit'] ?? 0), 4);
            if (($debit <= 0 && $credit <= 0) || ($debit > 0 && $credit > 0)) {
                throw new BadRequestException('Cada línea debe tener débito o crédito, no ambos');
            }

            $out[] = [
                'chart_account_id' => $accountId,
                'line_order' => $index,
                'description' => isset($line['description']) ? trim((string) $line['description']) : null,
                'debit' => $debit,
                'credit' => $credit,
                'debit_functional' => round($debit * $rateNum, 4),
                'credit_functional' => round($credit * $rateNum, 4),
            ];
        }

        return $out;
    }

    /** @param list<array<string, mixed>> $lines */
    private function assertBalanced(array $lines): void
    {
        $debit = 0.0;
        $credit = 0.0;
        foreach ($lines as $line) {
            $debit += (float) $line['debit'];
            $credit += (float) $line['credit'];
        }

        if (round($debit, 4) !== round($credit, 4)) {
            throw new BadRequestException('El asiento no cuadra: débitos y créditos deben ser iguales');
        }
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapEntrySummary(array $row): array
    {
        return [
            'id' => $row['id'],
            'entry_number' => $row['entry_number'],
            'entry_date' => $row['entry_date'],
            'description' => $row['description'],
            'status' => $row['status'],
            'operating_currency' => $row['operating_currency'],
            'functional_currency' => $row['functional_currency'],
            'exchange_rate' => (string) $row['exchange_rate'],
        ];
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapEntryDetail(array $row): array
    {
        $lines = array_map(static fn (array $line): array => [
            'id' => $line['id'],
            'chart_account_id' => $line['chart_account_id'],
            'account_code' => $line['account_code'] ?? '',
            'account_name' => $line['account_name'] ?? '',
            'line_order' => (int) ($line['line_order'] ?? 0),
            'description' => $line['description'],
            'debit' => (float) $line['debit'],
            'credit' => (float) $line['credit'],
            'debit_functional' => (float) $line['debit_functional'],
            'credit_functional' => (float) $line['credit_functional'],
        ], $this->journals->listLinesByEntry((string) $row['id']));

        return [
            ...$this->mapEntrySummary($row),
            'branch_id' => $row['branch_id'],
            'lines' => $lines,
        ];
    }
}
