<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\InvoiceRepositoryInterface;
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 InvoiceService
{
    public function __construct(
        private readonly InvoiceRepositoryInterface $repository,
        private readonly JournalRepositoryInterface $journals,
        private readonly TaxRateRepositoryInterface $taxRates,
        private readonly JournalService $journalService,
        private readonly PDO $pdo
    ) {}

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

    /** @return array<string, mixed> */
    public function getInvoice(string $companyId, string $id): array
    {
        $invoice = $this->repository->findById($id);
        if ($invoice === null || (string) $invoice['company_id'] !== $companyId) {
            throw new NotFoundException('Factura de venta no encontrada');
        }

        return $this->mapInvoiceDetail($invoice);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function saveInvoice(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('Factura de venta no encontrada');
                }
                if (in_array((string) $existing['status'], ['paid', 'partially_paid'])) {
                    throw new BadRequestException('No se puede modificar una factura que tiene cobros aplicados. Primero anula o elimina los cobros.');
                }
                $invoiceNumber = (string) $existing['invoice_number'];
            } else {
                $id = Database::uuid();
                $invoiceNumber = $this->repository->nextInvoiceNumber($companyId);
            }

            $customerId = trim((string) ($data['customer_id'] ?? ''));
            $receivableAccountId = trim((string) ($data['receivable_account_id'] ?? ''));
            $invoiceDate = trim((string) ($data['invoice_date'] ?? date('Y-m-d')));
            $dueDate = trim((string) ($data['due_date'] ?? date('Y-m-d')));
            $status = trim((string) ($data['status'] ?? 'draft'));
            $reference = isset($data['reference']) ? trim((string) $data['reference']) : '';
            $notes = isset($data['notes']) ? trim((string) $data['notes']) : '';
            $discountAmount = (float) ($data['discount_amount'] ?? 0.0);
            $lines = $data['lines'] ?? [];

            $this->validateInvoiceFields($customerId, $receivableAccountId, $invoiceDate, $dueDate, $lines);

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

            // Process lines & compute totals
            $processedLines = [];
            $subtotal = 0.0;
            $taxAmount = 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 de ingresos 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,
                ];

                $subtotal += $lineSubtotal;
                $taxAmount += $lineTaxAmount;
            }

            $total = round($subtotal + $taxAmount - $discountAmount, 4);
            if ($total < 0) {
                throw new BadRequestException('El total de la factura no puede ser negativo');
            }

            // Save Invoice Header
            $invoiceObj = [
                'id' => $id,
                'company_id' => $companyId,
                'branch_id' => $branchId,
                'customer_id' => $customerId,
                'receivable_account_id' => $receivableAccountId,
                'invoice_number' => $invoiceNumber,
                'invoice_date' => $invoiceDate,
                'due_date' => $dueDate,
                'reference' => $reference,
                'currency_code' => $currencyCode,
                'exchange_rate' => $exchangeRate,
                'discount_amount' => $discountAmount,
                'tax_amount' => $taxAmount,
                'subtotal' => $subtotal,
                'total' => $total,
                'balance' => $total, // Balance starts equal to total
                'status' => $status,
                'notes' => $notes,
                'created_by' => $userId,
            ];

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

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

            // Manage Accounting Integration (Auto-Posting)
            $this->deleteJournalEntryForInvoice($companyId, $invoiceNumber);

            if ($status === 'posted') {
                $this->postInvoiceToAccounting($companyId, $branchId, $userId, $invoiceObj, $processedLines);
            }

            $this->pdo->commit();

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

    public function deleteInvoice(string $companyId, string $id): void
    {
        $invoice = $this->repository->findById($id);
        if ($invoice === null || (string) $invoice['company_id'] !== $companyId) {
            throw new NotFoundException('Factura de venta no encontrada');
        }
        if (in_array((string) $invoice['status'], ['paid', 'partially_paid'])) {
            throw new BadRequestException('No se puede eliminar una factura que tiene cobros aplicados. Primero anula o elimina los cobros.');
        }

        $this->pdo->beginTransaction();
        try {
            $this->deleteJournalEntryForInvoice($companyId, (string) $invoice['invoice_number']);
            $this->repository->deleteLines($id);
            $this->repository->delete($id);
            $this->pdo->commit();
        } catch (\Exception $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }
    }

    /** @return list<array<string, mixed>> */
    public function listUnpaidByCustomer(string $companyId, string $customerId): array
    {
        return $this->repository->listUnpaidByCustomer($companyId, $customerId);
    }

    private function postInvoiceToAccounting(string $companyId, ?string $branchId, ?string $userId, array $invoice, array $lines): void
    {
        $rate = (float) $invoice['exchange_rate'];
        $jeId = Database::uuid();

        // 1. Debit Receivable Account
        $journalLines = [
            [
                'chart_account_id' => $invoice['receivable_account_id'],
                'line_order' => 0,
                'description' => "Cuentas por Cobrar - Factura #{$invoice['invoice_number']}",
                'debit' => $invoice['total'],
                'credit' => 0.0,
                'debit_functional' => round($invoice['total'] * $rate, 4),
                'credit_functional' => 0.0,
            ]
        ];

        // 2. Credit Revenue Account per line
        $order = 1;
        foreach ($lines as $line) {
            $lineSubtotal = $line['quantity'] * $line['price'];
            $journalLines[] = [
                'chart_account_id' => $line['chart_account_id'],
                'line_order' => $order++,
                'description' => $line['description'] ?: "Venta de Bienes/Servicios - Factura #{$invoice['invoice_number']}",
                'debit' => 0.0,
                'credit' => $lineSubtotal,
                'debit_functional' => 0.0,
                'credit_functional' => round($lineSubtotal * $rate, 4),
            ];
        }

        // 3. Credit Tax Account (if tax exists)
        if ($invoice['tax_amount'] > 0) {
            $taxAccount = $this->resolveTaxAccount($companyId);
            $journalLines[] = [
                'chart_account_id' => $taxAccount,
                'line_order' => $order++,
                'description' => "Impuesto Generado - Factura #{$invoice['invoice_number']}",
                'debit' => 0.0,
                'credit' => $invoice['tax_amount'],
                'debit_functional' => 0.0,
                'credit_functional' => round($invoice['tax_amount'] * $rate, 4),
            ];
        }

        // 4. Debit Discount Account (if discount exists)
        if ($invoice['discount_amount'] > 0) {
            $discountAccount = $this->resolveDiscountAccount($companyId);
            $journalLines[] = [
                'chart_account_id' => $discountAccount,
                'line_order' => $order++,
                'description' => "Descuento Concedido - Factura #{$invoice['invoice_number']}",
                'debit' => $invoice['discount_amount'],
                'credit' => 0.0,
                'debit_functional' => round($invoice['discount_amount'] * $rate, 4),
                'credit_functional' => 0.0,
            ];
        }

        // Insert Journal Entry
        $this->journals->insertEntry(
            $jeId,
            $companyId,
            $branchId,
            (string) $invoice['invoice_number'],
            (string) $invoice['invoice_date'],
            "Registro contable automático - Factura #{$invoice['invoice_number']}",
            'posted',
            (string) $invoice['currency_code'],
            (string) $invoice['currency_code'],
            (string) $rate,
            $userId
        );

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

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

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

    private function resolveTaxAccount(string $companyId): string
    {
        // Lookup liability account starting with '24' or '2' for taxes
        $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 la factura: Se requiere configurar una cuenta contable de pasivo/impuestos.");
    }

    private function resolveDiscountAccount(string $companyId): string
    {
        // Lookup expense account (starts with '5' or is expense)
        $st = $this->pdo->prepare(
            "SELECT id FROM chart_accounts 
             WHERE company_id = ? AND account_type = 'expense' 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 la factura: Se requiere configurar una cuenta contable de gastos para descuentos.");
    }

    private function validateInvoiceFields(string $customerId, string $receivableAccountId, string $date, string $dueDate, array $lines): void
    {
        if ($customerId === '') {
            throw new BadRequestException('El cliente es obligatorio');
        }
        if ($receivableAccountId === '') {
            throw new BadRequestException('La cuenta por cobrar es obligatoria');
        }
        if ($date === '') {
            throw new BadRequestException('La fecha de la factura es obligatoria');
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            throw new BadRequestException('Formato de fecha de factura inválido. Utilice AAAA-MM-DD');
        }
        if ($dueDate === '') {
            throw new BadRequestException('La fecha de vencimiento es obligatoria');
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dueDate)) {
            throw new BadRequestException('Formato de fecha de vencimiento inválido. Utilice AAAA-MM-DD');
        }
        if (count($lines) === 0) {
            throw new BadRequestException('La factura debe tener al menos una línea de detalle');
        }
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapInvoiceSummary(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'] ?? '',
            'receivable_account_id' => $row['receivable_account_id'],
            'invoice_number' => $row['invoice_number'],
            'invoice_date' => $row['invoice_date'],
            'due_date' => $row['due_date'],
            'reference' => $row['reference'] ?? '',
            'currency_code' => $row['currency_code'] ?? 'USD',
            'exchange_rate' => (float) ($row['exchange_rate'] ?? 1.0),
            'discount_amount' => (float) ($row['discount_amount'] ?? 0.0),
            'tax_amount' => (float) ($row['tax_amount'] ?? 0.0),
            'subtotal' => (float) ($row['subtotal'] ?? 0.0),
            'total' => (float) ($row['total'] ?? 0.0),
            'balance' => (float) ($row['balance'] ?? 0.0),
            'status' => $row['status'] ?? 'draft',
            'notes' => $row['notes'] ?? '',
        ];
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapInvoiceDetail(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->findLinesByInvoiceId((string) $row['id']));

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