<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\ExpenseRepositoryInterface;
use ApiModel\Domain\Repository\JournalRepositoryInterface;
use ApiModel\Domain\Repository\TaxRateRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\NotFoundException;
use PDO;

final class ExpenseService
{
    public function __construct(
        private readonly ExpenseRepositoryInterface $repository,
        private readonly JournalRepositoryInterface $journals,
        private readonly TaxRateRepositoryInterface $taxRates,
        private readonly JournalService $journalService,
        private readonly PDO $pdo
    ) {}

    /** @return list<array<string, mixed>> */
    public function listExpenses(string $companyId, ?string $branchId = null): array
    {
        return array_map(fn (array $row): array => $this->mapExpenseSummary($row), $this->repository->listByCompany($companyId, $branchId));
    }

    /** @return array<string, mixed> */
    public function getExpense(string $companyId, string $id): array
    {
        $expense = $this->repository->findById($id);
        if ($expense === null || (string) $expense['company_id'] !== $companyId) {
            throw new NotFoundException('Gasto de caja no encontrado');
        }

        return $this->mapExpenseDetail($expense);
    }

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

        $this->pdo->beginTransaction();

        try {
            if ($isUpdate) {
                $existing = $this->repository->findById($id);
                if ($existing === null || (string) $existing['company_id'] !== $companyId) {
                    throw new NotFoundException('Gasto de caja no encontrado');
                }
                $expenseNumber = (string) $existing['expense_number'];
            } else {
                $id = Database::uuid();
                $expenseNumber = $this->repository->nextExpenseNumber($companyId);
            }

            $chartAccountId = trim((string) ($data['chart_account_id'] ?? ''));
            $paymentDate = trim((string) ($data['payment_date'] ?? date('Y-m-d')));
            $paymentMethod = trim((string) ($data['payment_method'] ?? 'cash'));
            $notes = isset($data['notes']) ? trim((string) $data['notes']) : '';
            $lines = $data['lines'] ?? [];

            $this->validateExpenseFields($chartAccountId, $paymentDate, $lines);

            // Resolve currency & exchange rates using JournalService
            $currencies = $this->journalService->resolveCurrencies($companyId, $branchId, $paymentDate);
            $currencyCode = $currencies['operating_currency'];
            $exchangeRate = (float) $currencies['exchange_rate'];

            // Process lines & compute totals
            $processedLines = [];
            $amount = 0.0;

            foreach ($lines as $line) {
                $qty = (float) ($line['quantity'] ?? 1.0);
                $price = (float) ($line['price'] ?? 0.0);
                $lineTaxRateId = isset($line['tax_rate_id']) && trim((string) ($line['tax_rate_id'])) !== '' ? trim((string) $line['tax_rate_id']) : null;
                $lineChartAccountId = trim((string) ($line['chart_account_id'] ?? ''));

                if ($qty <= 0) {
                    throw new BadRequestException('La cantidad debe ser mayor a 0');
                }
                if ($price < 0) {
                    throw new BadRequestException('El precio no puede ser negativo');
                }
                if ($lineChartAccountId === '') {
                    throw new BadRequestException('Cada línea de detalle debe tener una cuenta contable de gasto/costo asignada');
                }

                $lineSubtotal = $qty * $price;
                $lineTaxAmount = 0.0;

                if ($lineTaxRateId !== null) {
                    $tax = $this->taxRates->findById($lineTaxRateId);
                    if ($tax !== null) {
                        $ratePercent = (float) ($tax['rate'] ?? 0.0);
                        $lineTaxAmount = round($lineSubtotal * ($ratePercent / 100.0), 4);
                    }
                }

                $lineTotal = $lineSubtotal + $lineTaxAmount;

                $processedLines[] = [
                    'id' => Database::uuid(),
                    'description' => isset($line['description']) ? trim((string) $line['description']) : '',
                    'quantity' => $qty,
                    'price' => $price,
                    'tax_rate_id' => $lineTaxRateId,
                    'tax_amount' => $lineTaxAmount,
                    'total' => $lineTotal,
                    'chart_account_id' => $lineChartAccountId,
                ];

                $amount += $lineTotal;
            }

            $amount = round($amount, 4);

            // Save Expense Header
            $expenseObj = [
                'id' => $id,
                'company_id' => $companyId,
                'branch_id' => $branchId,
                'expense_number' => $expenseNumber,
                'payment_date' => $paymentDate,
                'payment_method' => $paymentMethod,
                'amount' => $amount,
                'currency_code' => $currencyCode,
                'exchange_rate' => $exchangeRate,
                'chart_account_id' => $chartAccountId,
                'notes' => $notes,
                'created_by' => $userId,
            ];

            if ($isUpdate) {
                $this->repository->deleteLines($id);
                $this->repository->update($id, $expenseObj);
            } else {
                $this->repository->insert($expenseObj);
            }

            // Save Lines
            $this->repository->insertLines($id, $processedLines);

            // Manage Accounting Integration (Auto-Posting)
            $this->deleteJournalEntryForExpense($companyId, $expenseNumber);
            $this->postExpenseToAccounting($companyId, $branchId, $userId, $expenseObj, $processedLines);

            $this->pdo->commit();

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

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

        $this->pdo->beginTransaction();
        try {
            $this->deleteJournalEntryForExpense($companyId, (string) $expense['expense_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 postExpenseToAccounting(string $companyId, ?string $branchId, ?string $userId, array $expense, array $lines): void
    {
        $rate = (float) $expense['exchange_rate'];
        $jeId = Database::uuid();

        // 1. Credit Cash/Bank Account (Acreditar Caja/Banco)
        $journalLines = [
            [
                'chart_account_id' => $expense['chart_account_id'],
                'line_order' => 0,
                'description' => "Egreso Directo de Caja - Gasto #{$expense['expense_number']}",
                'debit' => 0.0,
                'credit' => $expense['amount'],
                'debit_functional' => 0.0,
                'credit_functional' => round($expense['amount'] * $rate, 4),
            ]
        ];

        // 2. Debit Expense accounts and taxes
        $order = 1;
        foreach ($lines as $line) {
            $lineSubtotal = $line['quantity'] * $line['price'];
            
            // Debit Expense Account
            $journalLines[] = [
                'chart_account_id' => $line['chart_account_id'],
                'line_order' => $order++,
                'description' => $line['description'] ?: "Gasto de Caja - Gasto #{$expense['expense_number']}",
                'debit' => $lineSubtotal,
                'credit' => 0.0,
                'debit_functional' => round($lineSubtotal * $rate, 4),
                'credit_functional' => 0.0,
            ];

            // Debit Tax Account if line has tax
            if ($line['tax_amount'] > 0) {
                $taxAccount = $this->resolveTaxAccount($companyId);
                $journalLines[] = [
                    'chart_account_id' => $taxAccount,
                    'line_order' => $order++,
                    'description' => "Impuesto Pagado - Gasto #{$expense['expense_number']}",
                    'debit' => $line['tax_amount'],
                    'credit' => 0.0,
                    'debit_functional' => round($line['tax_amount'] * $rate, 4),
                    'credit_functional' => 0.0,
                ];
            }
        }

        // Insert Journal Entry
        $this->journals->insertEntry(
            $jeId,
            $companyId,
            $branchId,
            (string) $expense['expense_number'],
            (string) $expense['payment_date'],
            "Registro contable automático - Gasto Directo #{$expense['expense_number']}",
            'posted',
            (string) $expense['currency_code'],
            (string) $expense['currency_code'],
            (string) $rate,
            $userId
        );

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

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

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

    private function resolveTaxAccount(string $companyId): string
    {
        // Lookup account starting with '24' first (typical tax code)
        $st = $this->pdo->prepare(
            "SELECT id FROM chart_accounts 
             WHERE company_id = ? AND account_type = 'liability' AND code LIKE '24%' AND is_header = FALSE 
             ORDER BY code ASC LIMIT 1"
        );
        $st->execute([$companyId]);
        $id = $st->fetchColumn();

        if ($id !== false) {
            return (string) $id;
        }

        // Fallback to first general liability account
        $st = $this->pdo->prepare(
            "SELECT id FROM chart_accounts 
             WHERE company_id = ? AND account_type = 'liability' AND is_header = FALSE 
             ORDER BY code ASC LIMIT 1"
        );
        $st->execute([$companyId]);
        $id = $st->fetchColumn();

        if ($id !== false) {
            return (string) $id;
        }

        throw new BadRequestException("No se pudo registrar el gasto: Se requiere configurar una cuenta contable de pasivo/impuestos.");
    }

    private function validateExpenseFields(string $chartAccountId, string $date, array $lines): void
    {
        if ($chartAccountId === '') {
            throw new BadRequestException('La cuenta de caja/banco es obligatoria');
        }
        if ($date === '') {
            throw new BadRequestException('La fecha de pago es obligatoria');
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            throw new BadRequestException('Formato de fecha de pago inválido. Utilice AAAA-MM-DD');
        }
        if (count($lines) === 0) {
            throw new BadRequestException('El egreso de caja debe tener al menos una línea de detalle');
        }
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapExpenseSummary(array $row): array
    {
        return [
            'id' => $row['id'],
            'company_id' => $row['company_id'],
            'branch_id' => $row['branch_id'],
            'expense_number' => $row['expense_number'],
            'payment_date' => $row['payment_date'],
            'payment_method' => $row['payment_method'],
            'amount' => (float) ($row['amount'] ?? 0.0),
            'currency_code' => $row['currency_code'] ?? 'USD',
            'exchange_rate' => (float) ($row['exchange_rate'] ?? 1.0),
            'chart_account_id' => $row['chart_account_id'],
            'account_code' => $row['account_code'] ?? '',
            'account_name' => $row['account_name'] ?? '',
            'notes' => $row['notes'] ?? '',
        ];
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapExpenseDetail(array $row): array
    {
        $lines = array_map(static fn (array $line): array => [
            'id' => $line['id'],
            'description' => $line['description'] ?? '',
            'quantity' => (float) ($line['quantity'] ?? 1.0),
            'price' => (float) ($line['price'] ?? 0.0),
            'tax_rate_id' => $line['tax_rate_id'] ?? null,
            'tax_name' => $line['tax_name'] ?? '',
            'tax_rate' => (float) ($line['tax_rate'] ?? 0.0),
            'tax_amount' => (float) ($line['tax_amount'] ?? 0.0),
            'total' => (float) ($line['total'] ?? 0.0),
            'chart_account_id' => $line['chart_account_id'],
            'account_code' => $line['account_code'] ?? '',
            'account_name' => $line['account_name'] ?? '',
        ], $this->repository->findLinesByExpenseId((string) $row['id']));

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