<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\BillRepositoryInterface;
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 ApiModel\Service\BudgetService;
use PDO;

final class BillService
{
    public function __construct(
        private readonly BillRepositoryInterface $repository,
        private readonly JournalRepositoryInterface $journals,
        private readonly TaxRateRepositoryInterface $taxRates,
        private readonly JournalService $journalService,
        private readonly PDO $pdo,
        private readonly BudgetService $budgetService
    ) {}

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

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

        return $this->mapBillDetail($bill);
    }

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

            $vendorId = trim((string) ($data['vendor_id'] ?? ''));
            $payableAccountId = trim((string) ($data['payable_account_id'] ?? ''));
            $billDate = trim((string) ($data['bill_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->validateBillFields($vendorId, $payableAccountId, $billDate, $dueDate, $lines);

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

            $purchaseOrderId = isset($data['purchase_order_id']) && trim((string) $data['purchase_order_id']) !== '' ? trim((string) $data['purchase_order_id']) : null;

            // Load linked Purchase/Service Order lines if present
            $poLines = [];
            if ($purchaseOrderId !== null) {
                $stPoL = $this->pdo->prepare("SELECT classifier_id FROM public.purchase_order_lines WHERE purchase_order_id = ? ORDER BY line_order ASC");
                $stPoL->execute([$purchaseOrderId]);
                $poLines = $stPoL->fetchAll(PDO::FETCH_ASSOC);
            }

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

            foreach ($lines as $idx => $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 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;

                // Inherit budget classifier from Purchase/Service Order if available
                $budgetClassifierId = isset($line['budget_classifier_id']) && trim((string) $line['budget_classifier_id']) !== '' ? trim((string) $line['budget_classifier_id']) : null;
                if ($budgetClassifierId === null && isset($poLines[$idx])) {
                    $budgetClassifierId = $poLines[$idx]['classifier_id'];
                }

                $budgetOrgUnitId = isset($line['budget_org_unit_id']) && trim((string) $line['budget_org_unit_id']) !== '' ? trim((string) $line['budget_org_unit_id']) : null;
                if ($budgetOrgUnitId === null && $budgetClassifierId !== null) {
                    $stOrg = $this->pdo->prepare("SELECT id FROM budget.organizational_units WHERE is_active = TRUE LIMIT 1");
                    $stOrg->execute();
                    $budgetOrgUnitId = $stOrg->fetchColumn() ?: null;
                }

                $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,
                    'budget_classifier_id' => $budgetClassifierId,
                    'budget_org_unit_id' => $budgetOrgUnitId,
                ];

                $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 Bill Header
            $purchaseOrderId = isset($data['purchase_order_id']) && trim((string) $data['purchase_order_id']) !== '' ? trim((string) $data['purchase_order_id']) : null;

            $billObj = [
                'id' => $id,
                'company_id' => $companyId,
                'branch_id' => $branchId,
                'vendor_id' => $vendorId,
                'payable_account_id' => $payableAccountId,
                'bill_number' => $billNumber,
                'bill_date' => $billDate,
                '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,
                'purchase_order_id' => $purchaseOrderId,
            ];

            // Release pre-commitments if purchase order is attached and bill is posted
            if ($purchaseOrderId !== null && $status === 'posted') {
                $stPre = $this->pdo->prepare("UPDATE budget.pre_commitments SET status = 'executed', updated_at = CURRENT_TIMESTAMP WHERE source_document_id = ? AND status = 'active'");
                $stPre->execute([$purchaseOrderId]);

                $stPo = $this->pdo->prepare("UPDATE public.purchase_orders SET status = 'completed', updated_at = CURRENT_TIMESTAMP WHERE id = ?");
                $stPo->execute([$purchaseOrderId]);
            }

            // Integración de Presupuesto Público
            if ($isUpdate) {
                $this->budgetService->deleteExecutionsBySource('bill', $id);
            }

            $billYear = (int) date('Y', strtotime($billDate));
            $fy = $this->budgetService->findFiscalYearByYear($billYear);
            
            if ($fy !== null && $fy['status'] === 'open') {
                $fiscalYearId = $fy['id'];
                
                // 1. Validar disponibilidad de presupuesto en todas las líneas de gasto
                foreach ($processedLines as $line) {
                    $clId = $line['budget_classifier_id'] ?? null;
                    $ouId = $line['budget_org_unit_id'] ?? null;
                    if ($clId && $ouId) {
                        $this->budgetService->verifyAvailability($fiscalYearId, $ouId, $clId, (float) $line['total']);
                    }
                }
                
                // 2. Registrar el Devengado si está publicado (posted)
                if ($status === 'posted') {
                    foreach ($processedLines as $line) {
                        $clId = $line['budget_classifier_id'] ?? null;
                        $ouId = $line['budget_org_unit_id'] ?? null;
                        if ($clId && $ouId) {
                            $this->budgetService->recordExecution($userId, [
                                'fiscal_year_id' => $fiscalYearId,
                                'org_unit_id' => $ouId,
                                'classifier_id' => $clId,
                                'phase' => 'accrued',
                                'source_document_type' => 'bill',
                                'source_document_id' => $id,
                                'amount' => (float) $line['total'],
                                'description' => "Devengado Factura #{$billNumber} - L: {$line['description']}"
                            ]);
                        }
                    }
                }
            }

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

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

            // Manage Accounting Integration (Auto-Posting)
            $this->deleteJournalEntryForBill($companyId, $billNumber);

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

            $this->pdo->commit();

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

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

        $this->pdo->beginTransaction();
        try {
            $this->budgetService->deleteExecutionsBySource('bill', $id);
            $this->deleteJournalEntryForBill($companyId, (string) $bill['bill_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 listUnpaidByVendor(string $companyId, string $vendorId): array
    {
        return $this->repository->listUnpaidByVendor($companyId, $vendorId);
    }

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

        // 1. Credit Payable Account (Liability)
        $journalLines = [
            [
                'chart_account_id' => $bill['payable_account_id'],
                'line_order' => 0,
                'description' => "Cuentas por Pagar Proveedores - Factura #{$bill['bill_number']}",
                'debit' => 0.0,
                'credit' => $bill['total'],
                'debit_functional' => 0.0,
                'credit_functional' => round($bill['total'] * $rate, 4),
            ]
        ];

        // 2. Debit Cost/Expense 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'] ?: "Compra/Gasto - Factura #{$bill['bill_number']}",
                'debit' => $lineSubtotal,
                'credit' => 0.0,
                'debit_functional' => round($lineSubtotal * $rate, 4),
                'credit_functional' => 0.0,
            ];
        }

        // 3. Debit Tax Account (if tax exists - Purchase/Creditable Tax)
        if ($bill['tax_amount'] > 0) {
            $taxAccount = $this->resolveTaxAccount($companyId);
            $journalLines[] = [
                'chart_account_id' => $taxAccount,
                'line_order' => $order++,
                'description' => "Impuesto Pagado en Compras - Factura #{$bill['bill_number']}",
                'debit' => $bill['tax_amount'],
                'credit' => 0.0,
                'debit_functional' => round($bill['tax_amount'] * $rate, 4),
                'credit_functional' => 0.0,
            ];
        }

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

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

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

    private function deleteJournalEntryForBill(string $companyId, string $billNumber): void
    {
        $st = $this->pdo->prepare('SELECT id FROM journal_entries WHERE company_id = ? AND entry_number = ? LIMIT 1');
        $st->execute([$companyId, $billNumber]);
        $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 la factura: Se requiere configurar una cuenta contable de pasivo/impuestos.");
    }

    private function resolveDiscountAccount(string $companyId): string
    {
        // Lookup income account (starts with '4' or is income)
        $st = $this->pdo->prepare(
            "SELECT id FROM chart_accounts 
             WHERE company_id = ? AND account_type = 'income' 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 ingresos para descuentos.");
    }

    private function validateBillFields(string $vendorId, string $payableAccountId, string $date, string $dueDate, array $lines): void
    {
        if ($vendorId === '') {
            throw new BadRequestException('El proveedor es obligatorio');
        }
        if ($payableAccountId === '') {
            throw new BadRequestException('La cuenta por pagar 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 mapBillSummary(array $row): array
    {
        return [
            'id' => $row['id'],
            'company_id' => $row['company_id'],
            'branch_id' => $row['branch_id'],
            'vendor_id' => $row['vendor_id'],
            'vendor_name' => $row['vendor_name'] ?? '',
            'payable_account_id' => $row['payable_account_id'],
            'bill_number' => $row['bill_number'],
            'bill_date' => $row['bill_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'] ?? '',
            'purchase_order_id' => $row['purchase_order_id'] ?? null,
        ];
    }

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

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