<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\InvoiceRepositoryInterface;
use ApiModel\Domain\Repository\JournalRepositoryInterface;
use ApiModel\Domain\Repository\PaymentRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\NotFoundException;
use PDO;

final class PaymentService
{
    public function __construct(
        private readonly PaymentRepositoryInterface $repository,
        private readonly InvoiceRepositoryInterface $invoices,
        private readonly JournalRepositoryInterface $journals,
        private readonly JournalService $journalService,
        private readonly PDO $pdo
    ) {}

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

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

        return $this->mapPaymentDetail($payment);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function savePayment(string $companyId, ?string $branchId, ?string $userId, array $data): array
    {
        $id = Database::uuid();
        $paymentNumber = $this->repository->nextPaymentNumber($companyId);

        $customerId = trim((string) ($data['customer_id'] ?? ''));
        $chartAccountId = trim((string) ($data['chart_account_id'] ?? ''));
        $paymentDate = trim((string) ($data['payment_date'] ?? date('Y-m-d')));
        $paymentMethod = trim((string) ($data['payment_method'] ?? 'cash'));
        $amount = (float) ($data['amount'] ?? 0.0);
        $notes = isset($data['notes']) ? trim((string) $data['notes']) : '';
        $lines = $data['lines'] ?? []; // Allocations array: list of ['invoice_id' => ..., 'amount' => ...]
        $withholdingAmount = (float) ($data['withholding_amount'] ?? 0.0);
        $withholdingAccountId = isset($data['withholding_account_id']) && trim((string) $data['withholding_account_id']) !== '' ? trim((string) $data['withholding_account_id']) : null;

        $this->validatePaymentFields($customerId, $chartAccountId, $paymentDate, $amount, $lines);
        
        if ($withholdingAmount < 0) {
            throw new BadRequestException('El monto de retención no puede ser negativo');
        }
        if ($withholdingAmount > 0 && $withholdingAccountId === null) {
            throw new BadRequestException('Debe seleccionar una cuenta contable para registrar la retención');
        }

        $this->pdo->beginTransaction();

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

            // Save Payment Header
            $paymentObj = [
                'id' => $id,
                'company_id' => $companyId,
                'branch_id' => $branchId,
                'customer_id' => $customerId,
                'payment_number' => $paymentNumber,
                'payment_date' => $paymentDate,
                'payment_method' => $paymentMethod,
                'amount' => $amount,
                'currency_code' => $currencyCode,
                'exchange_rate' => $exchangeRate,
                'chart_account_id' => $chartAccountId,
                'notes' => $notes,
                'created_by' => $userId,
                'withholding_amount' => $withholdingAmount,
                'withholding_account_id' => $withholdingAccountId,
            ];
            $this->repository->insert($paymentObj);

            // Process invoice allocations
            $processedLines = [];
            $totalAllocated = 0.0;
            $journalCreditLines = [];
            $lineOrder = ($withholdingAmount > 0) ? 2 : 1;

            foreach ($lines as $line) {
                $invoiceId = trim((string) ($line['invoice_id'] ?? ''));
                $allocatedAmount = (float) ($line['amount'] ?? 0.0);

                if ($invoiceId === '') {
                    throw new BadRequestException('ID de factura inválido en la distribución del cobro');
                }
                if ($allocatedAmount <= 0) {
                    throw new BadRequestException('El monto a aplicar debe ser mayor a 0');
                }

                $invoice = $this->invoices->findById($invoiceId);
                if ($invoice === null || (string) $invoice['company_id'] !== $companyId || (string) $invoice['customer_id'] !== $customerId) {
                    throw new BadRequestException("Factura inválida o no pertenece al cliente seleccionado");
                }

                $currentBalance = (float) $invoice['balance'];
                if ($allocatedAmount > round($currentBalance, 4)) {
                    throw new BadRequestException("El monto aplicado ({$allocatedAmount}) excede el saldo restante de la factura #{$invoice['invoice_number']} ({$currentBalance})");
                }

                // Update invoice balance & status
                $newBalance = round($currentBalance - $allocatedAmount, 4);
                $newStatus = $newBalance <= 0.0001 ? 'paid' : 'partially_paid';

                $this->invoices->updateStatusAndBalance($invoiceId, $newStatus, (string) $newBalance);

                $processedLines[] = [
                    'id' => Database::uuid(),
                    'invoice_id' => $invoiceId,
                    'amount' => $allocatedAmount,
                ];

                $totalAllocated += $allocatedAmount;

                // Credit Line for Journal Entry: Credit the invoice's Receivable Account
                $journalCreditLines[] = [
                    'chart_account_id' => $invoice['receivable_account_id'],
                    'line_order' => $lineOrder++,
                    'description' => "Cobro aplicado a Factura #{$invoice['invoice_number']}",
                    'debit' => 0.0,
                    'credit' => $allocatedAmount,
                    'debit_functional' => 0.0,
                    'credit_functional' => round($allocatedAmount * $exchangeRate, 4),
                ];
            }

            if (round($totalAllocated, 4) !== round($amount + $withholdingAmount, 4)) {
                throw new BadRequestException("El total aplicado a facturas ({$totalAllocated}) debe coincidir exactamente con el monto neto recibido más la retención (" . ($amount + $withholdingAmount) . ")");
            }

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

            // Auto-Post Contabilidad (Journal Entry)
            $jeId = Database::uuid();

            // Debit Line: Debit the Cash/Bank account for net amount
            $journalLines = [
                [
                    'chart_account_id' => $chartAccountId,
                    'line_order' => 0,
                    'description' => "Recibo de Cobro #{$paymentNumber}",
                    'debit' => $amount,
                    'credit' => 0.0,
                    'debit_functional' => round($amount * $exchangeRate, 4),
                    'credit_functional' => 0.0,
                ]
            ];

            // If withholding exists, add Debit Line for withholding
            if ($withholdingAmount > 0 && $withholdingAccountId !== null) {
                $journalLines[] = [
                    'chart_account_id' => $withholdingAccountId,
                    'line_order' => 1,
                    'description' => "Retención de impuestos en Cobro #{$paymentNumber}",
                    'debit' => $withholdingAmount,
                    'credit' => 0.0,
                    'debit_functional' => round($withholdingAmount * $exchangeRate, 4),
                    'credit_functional' => 0.0,
                ];
            }

            $journalLines = array_merge($journalLines, $journalCreditLines);

            $this->journals->insertEntry(
                $jeId,
                $companyId,
                $branchId,
                $paymentNumber,
                $paymentDate,
                "Registro contable de cobro de clientes #{$paymentNumber}",
                'posted',
                $currencyCode,
                $currencyCode,
                (string) $exchangeRate,
                $userId
            );
            $this->journals->insertLines($jeId, $journalLines);

            $this->pdo->commit();

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

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

        $this->pdo->beginTransaction();

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

            // Revert balances of allocated invoices
            foreach ($lines as $line) {
                $invoiceId = (string) $line['invoice_id'];
                $allocatedAmount = (float) $line['amount'];

                $invoice = $this->invoices->findById($invoiceId);
                if ($invoice !== null) {
                    $newBalance = round((float) $invoice['balance'] + $allocatedAmount, 4);
                    $total = (float) $invoice['total'];
                    
                    // If balance equals total, status is 'posted', else 'partially_paid'
                    $newStatus = abs($newBalance - $total) < 0.0001 ? 'posted' : 'partially_paid';

                    $this->invoices->updateStatusAndBalance($invoiceId, $newStatus, (string) $newBalance);
                }
            }

            // Delete Journal Entry
            $paymentNumber = (string) $payment['payment_number'];
            $st = $this->pdo->prepare('SELECT id FROM journal_entries WHERE company_id = ? AND entry_number = ? LIMIT 1');
            $st->execute([$companyId, $paymentNumber]);
            $jeId = $st->fetchColumn();

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

            // Delete Payment Allocations & Payment Header
            $this->repository->delete($id);

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

    private function validatePaymentFields(string $customerId, string $chartAccountId, string $date, float $amount, array $lines): void
    {
        if ($customerId === '') {
            throw new BadRequestException('El cliente es obligatorio');
        }
        if ($chartAccountId === '') {
            throw new BadRequestException('La cuenta de caja/banco es obligatoria');
        }
        if ($date === '') {
            throw new BadRequestException('La fecha de cobro es obligatoria');
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            throw new BadRequestException('Formato de fecha inválido. Utilice AAAA-MM-DD');
        }
        if ($amount <= 0) {
            throw new BadRequestException('El monto cobrado debe ser mayor a 0');
        }
        if (count($lines) === 0) {
            throw new BadRequestException('Debes distribuir el cobro en al menos una factura con saldo pendiente');
        }
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapPaymentSummary(array $row): array
    {
        return [
            'id' => $row['id'],
            'company_id' => $row['company_id'],
            'branch_id' => $row['branch_id'],
            'customer_id' => $row['customer_id'],
            'customer_name' => $row['customer_name'] ?? '',
            'payment_number' => $row['payment_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'] ?? '',
            'withholding_amount' => (float) ($row['withholding_amount'] ?? 0.0),
            'withholding_account_id' => $row['withholding_account_id'] ?? null,
            'withholding_account_code' => $row['withholding_account_code'] ?? '',
            'withholding_account_name' => $row['withholding_account_name'] ?? '',
        ];
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapPaymentDetail(array $row): array
    {
        $lines = array_map(static fn (array $line): array => [
            'id' => $line['id'],
            'invoice_id' => $line['invoice_id'],
            'invoice_number' => $line['invoice_number'] ?? '',
            'invoice_total' => (float) ($line['invoice_total'] ?? 0.0),
            'invoice_balance' => (float) ($line['invoice_balance'] ?? 0.0),
            'amount' => (float) ($line['amount'] ?? 0.0),
        ], $this->repository->findLinesByPaymentId((string) $row['id']));

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