<?php

declare(strict_types=1);

namespace ApiModel\Service;

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

    /** @return list<array<string, mixed>> */
    public function listAdjustments(string $companyId, string $type): array
    {
        return array_map(fn (array $row): array => $this->mapAdjustmentSummary($row), $this->repository->listByCompany($companyId, $type));
    }

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

        return $this->mapAdjustmentDetail($adjustment);
    }

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

        $this->pdo->beginTransaction();

        try {
            $type = trim((string) ($data['adjustment_type'] ?? 'credit_note'));
            if (!in_array($type, ['credit_note', 'debit_note'], true)) {
                throw new BadRequestException('Tipo de ajuste inválido. Permitidos: credit_note, debit_note');
            }

            if ($isUpdate) {
                $existing = $this->repository->findById($id);
                if ($existing === null || (string) $existing['company_id'] !== $companyId) {
                    throw new NotFoundException('Nota de ajuste no encontrada');
                }
                if ((string) $existing['status'] === 'posted') {
                    throw new BadRequestException('No se puede modificar una nota de ajuste que ya ha sido contabilizada');
                }
                $documentNumber = (string) $existing['document_number'];
            } else {
                $id = Database::uuid();
                $documentNumber = $this->repository->nextDocumentNumber($companyId, $type);
            }

            $customerId = trim((string) ($data['customer_id'] ?? ''));
            $receivableAccountId = trim((string) ($data['receivable_account_id'] ?? ''));
            $documentDate = trim((string) ($data['document_date'] ?? date('Y-m-d')));
            $referenceInvoiceId = isset($data['reference_invoice_id']) && trim((string) $data['reference_invoice_id']) !== '' ? trim((string) $data['reference_invoice_id']) : null;
            $status = trim((string) ($data['status'] ?? 'draft'));
            $notes = isset($data['notes']) ? trim((string) $data['notes']) : '';
            $discountAmount = (float) ($data['discount_amount'] ?? 0.0);
            $lines = $data['lines'] ?? [];

            $this->validateAdjustmentFields($customerId, $receivableAccountId, $documentDate, $lines);

            // Fetch reference invoice if defined
            $refInvoice = null;
            if ($referenceInvoiceId !== null) {
                $refInvoice = $this->invoices->findById($referenceInvoiceId);
                if ($refInvoice === null || (string) $refInvoice['company_id'] !== $companyId) {
                    throw new BadRequestException('La factura de referencia seleccionada no es válida');
                }
            }

            // Resolve currency and exchange rates using JournalService
            $currencies = $this->journalService->resolveCurrencies($companyId, $branchId, $documentDate);
            $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 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;

                $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 nota de ajuste no puede ser negativo');
            }

            // Save Adjustment Header
            $adjustmentObj = [
                'id' => $id,
                'company_id' => $companyId,
                'branch_id' => $branchId,
                'customer_id' => $customerId,
                'adjustment_type' => $type,
                'document_number' => $documentNumber,
                'document_date' => $documentDate,
                'reference_invoice_id' => $referenceInvoiceId,
                'receivable_account_id' => $receivableAccountId,
                'discount_amount' => $discountAmount,
                'tax_amount' => $taxAmount,
                'subtotal' => $subtotal,
                'total' => $total,
                'status' => $status,
                'journal_entry_id' => null,
                'notes' => $notes,
                'created_by' => $userId,
            ];

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

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

            // Handle Accounting Auto-Posting if status is posted
            if ($status === 'posted') {
                // If it is a credit note and has reference invoice, validate and update invoice balance
                if ($refInvoice !== null) {
                    $currentBalance = (float) $refInvoice['balance'];
                    if ($type === 'credit_note') {
                        if ($total > round($currentBalance, 4)) {
                            throw new BadRequestException("El total de la Nota de Crédito ({$total}) no puede exceder el saldo de la factura #{$refInvoice['invoice_number']} ({$currentBalance})");
                        }
                        $newBalance = round($currentBalance - $total, 4);
                        $newStatus = $newBalance <= 0.0001 ? 'paid' : 'partially_paid';
                    } else { // debit_note
                        $newBalance = round($currentBalance + $total, 4);
                        // Status could stay partially_paid or posted (if previously paid)
                        $newStatus = 'partially_paid';
                    }
                    $this->invoices->updateStatusAndBalance($referenceInvoiceId, $newStatus, (string) $newBalance);
                }

                $journalEntryId = $this->postAdjustmentToAccounting($companyId, $branchId, $userId, $adjustmentObj, $processedLines);
                $this->repository->updateStatus($id, 'posted', $journalEntryId);
            }

            $this->pdo->commit();

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

    public function deleteAdjustment(string $companyId, string $id): void
    {
        $adjustment = $this->repository->findById($id);
        if ($adjustment === null || (string) $adjustment['company_id'] !== $companyId) {
            throw new NotFoundException('Nota de ajuste no encontrada');
        }

        $this->pdo->beginTransaction();

        try {
            $status = (string) $adjustment['status'];
            $type = (string) $adjustment['adjustment_type'];
            $total = (float) $adjustment['total'];
            $referenceInvoiceId = $adjustment['reference_invoice_id'] ?: null;

            if ($status === 'posted') {
                // Revert balance in reference invoice
                if ($referenceInvoiceId !== null) {
                    $invoice = $this->invoices->findById($referenceInvoiceId);
                    if ($invoice !== null) {
                        $currentBalance = (float) $invoice['balance'];
                        if ($type === 'credit_note') {
                            $newBalance = round($currentBalance + $total, 4);
                        } else { // debit_note
                            $newBalance = round($currentBalance - $total, 4);
                            if ($newBalance < 0) {
                                throw new BadRequestException("No se puede eliminar la Nota de Débito porque causaría un saldo negativo en la factura.");
                            }
                        }
                        $totalInvoice = (float) $invoice['total'];
                        $newStatus = abs($newBalance - $totalInvoice) < 0.0001 ? 'posted' : 'partially_paid';
                        if ($newBalance <= 0.0001) {
                            $newStatus = 'paid';
                        }
                        $this->invoices->updateStatusAndBalance($referenceInvoiceId, $newStatus, (string) $newBalance);
                    }
                }

                // Delete Journal Entry
                $journalEntryId = $adjustment['journal_entry_id'] ?: null;
                if ($journalEntryId !== null) {
                    $this->journals->deleteLinesByEntry((string) $journalEntryId);
                    $this->journals->deleteEntry((string) $journalEntryId);
                }
            }

            $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 postAdjustmentToAccounting(string $companyId, ?string $branchId, ?string $userId, array $adjustment, array $lines): string
    {
        $rate = (float) $adjustment['exchange_rate'];
        $jeId = Database::uuid();
        $type = (string) $adjustment['adjustment_type'];
        $total = (float) $adjustment['total'];

        $journalLines = [];
        $lineOrder = 0;

        if ($type === 'credit_note') {
            // Credit Note reduces Receivables (Credit) and reduces Revenue (Debit)
            
            // 1. Debit Revenue Account per line
            foreach ($lines as $line) {
                $lineSubtotal = $line['quantity'] * $line['price'];
                $journalLines[] = [
                    'chart_account_id' => $line['chart_account_id'],
                    'line_order' => $lineOrder++,
                    'description' => $line['description'] ?: "Devolución/Ajuste de Venta - Nota de Crédito #{$adjustment['document_number']}",
                    'debit' => $lineSubtotal,
                    'credit' => 0.0,
                    'debit_functional' => round($lineSubtotal * $rate, 4),
                    'credit_functional' => 0.0,
                ];
            }

            // 2. Debit Tax Account (if tax exists)
            if ($adjustment['tax_amount'] > 0) {
                $taxAccount = $this->resolveTaxAccount($companyId);
                $journalLines[] = [
                    'chart_account_id' => $taxAccount,
                    'line_order' => $lineOrder++,
                    'description' => "Impuesto Ajustado - Nota de Crédito #{$adjustment['document_number']}",
                    'debit' => $adjustment['tax_amount'],
                    'credit' => 0.0,
                    'debit_functional' => round($adjustment['tax_amount'] * $rate, 4),
                    'credit_functional' => 0.0,
                ];
            }

            // 3. Credit Discount Account (if discount exists)
            if ($adjustment['discount_amount'] > 0) {
                $discountAccount = $this->resolveDiscountAccount($companyId);
                $journalLines[] = [
                    'chart_account_id' => $discountAccount,
                    'line_order' => $lineOrder++,
                    'description' => "Descuento Nota de Crédito #{$adjustment['document_number']}",
                    'debit' => 0.0,
                    'credit' => $adjustment['discount_amount'],
                    'debit_functional' => 0.0,
                    'credit_functional' => round($adjustment['discount_amount'] * $rate, 4),
                ];
            }

            // 4. Credit Receivable Account (total)
            $journalLines[] = [
                'chart_account_id' => $adjustment['receivable_account_id'],
                'line_order' => $lineOrder++,
                'description' => "Reducción de Deuda - Nota de Crédito #{$adjustment['document_number']}",
                'debit' => 0.0,
                'credit' => $total,
                'debit_functional' => 0.0,
                'credit_functional' => round($total * $rate, 4),
            ];

        } else {
            // Debit Note increases Receivables (Debit) and increases Revenue (Credit)
            
            // 1. Debit Receivable Account (total)
            $journalLines[] = [
                'chart_account_id' => $adjustment['receivable_account_id'],
                'line_order' => $lineOrder++,
                'description' => "Incremento de Deuda - Nota de Débito #{$adjustment['document_number']}",
                'debit' => $total,
                'credit' => 0.0,
                'debit_functional' => round($total * $rate, 4),
                'credit_functional' => 0.0,
            ];

            // 2. Debit Discount Account (if discount exists)
            if ($adjustment['discount_amount'] > 0) {
                $discountAccount = $this->resolveDiscountAccount($companyId);
                $journalLines[] = [
                    'chart_account_id' => $discountAccount,
                    'line_order' => $lineOrder++,
                    'description' => "Descuento Nota de Débito #{$adjustment['document_number']}",
                    'debit' => $adjustment['discount_amount'],
                    'credit' => 0.0,
                    'debit_functional' => round($adjustment['discount_amount'] * $rate, 4),
                    'credit_functional' => 0.0,
                ];
            }

            // 3. Credit Revenue Account per line
            foreach ($lines as $line) {
                $lineSubtotal = $line['quantity'] * $line['price'];
                $journalLines[] = [
                    'chart_account_id' => $line['chart_account_id'],
                    'line_order' => $lineOrder++,
                    'description' => $line['description'] ?: "Recargo/Ingreso de Venta - Nota de Débito #{$adjustment['document_number']}",
                    'debit' => 0.0,
                    'credit' => $lineSubtotal,
                    'debit_functional' => 0.0,
                    'credit_functional' => round($lineSubtotal * $rate, 4),
                ];
            }

            // 4. Credit Tax Account (if tax exists)
            if ($adjustment['tax_amount'] > 0) {
                $taxAccount = $this->resolveTaxAccount($companyId);
                $journalLines[] = [
                    'chart_account_id' => $taxAccount,
                    'line_order' => $lineOrder++,
                    'description' => "Impuesto Generado - Nota de Débito #{$adjustment['document_number']}",
                    'debit' => 0.0,
                    'credit' => $adjustment['tax_amount'],
                    'debit_functional' => 0.0,
                    'credit_functional' => round($adjustment['tax_amount'] * $rate, 4),
                ];
            }
        }

        // Insert Journal Entry
        $this->journals->insertEntry(
            $jeId,
            $companyId,
            $branchId,
            (string) $adjustment['document_number'],
            (string) $adjustment['document_date'],
            ($type === 'credit_note' ? 'Nota de Crédito' : 'Nota de Débito') . " contable automática #{$adjustment['document_number']}",
            'posted',
            (string) $adjustment['currency_code'],
            (string) $adjustment['currency_code'],
            (string) $rate,
            $userId
        );

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

        return $jeId;
    }

    private function resolveTaxAccount(string $companyId): string
    {
        $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("Se requiere configurar una cuenta contable de pasivo/impuestos.");
    }

    private function resolveDiscountAccount(string $companyId): string
    {
        $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("Se requiere configurar una cuenta contable de gastos para descuentos.");
    }

    private function validateAdjustmentFields(string $customerId, string $receivableAccountId, string $date, 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 del documento 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 (count($lines) === 0) {
            throw new BadRequestException('El ajuste debe tener al menos una línea de detalle');
        }
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapAdjustmentSummary(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'] ?? '',
            'adjustment_type' => $row['adjustment_type'],
            'document_number' => $row['document_number'],
            'document_date' => $row['document_date'],
            'reference_invoice_id' => $row['reference_invoice_id'] ?? null,
            'reference_invoice_number' => $row['reference_invoice_number'] ?? '',
            'receivable_account_id' => $row['receivable_account_id'],
            '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),
            'status' => $row['status'] ?? 'draft',
            'journal_entry_id' => $row['journal_entry_id'] ?? null,
            'notes' => $row['notes'] ?? '',
        ];
    }

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

        return [
            ...$this->mapAdjustmentSummary($row),
            'lines' => $lines,
            'reference_invoice_balance' => isset($row['reference_invoice_balance']) ? (float) $row['reference_invoice_balance'] : null,
        ];
    }
}
