<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\PayrollRepositoryInterface;
use ApiModel\Domain\Repository\EmployeeRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\NotFoundException;
use PDO;

final class PayrollService
{
    public function __construct(
        private readonly PayrollRepositoryInterface $payrolls,
        private readonly EmployeeRepositoryInterface $employees,
        private readonly JournalService $journalService,
        private readonly PDO $pdo
    ) {}

    /** @return list<array<string, mixed>> */
    public function listPayrolls(string $companyId): array
    {
        return array_map($this->mapPayrollSummary(...), $this->payrolls->listByCompany($companyId));
    }

    /** @return array<string, mixed> */
    public function getPayroll(string $companyId, string $id): array
    {
        $payroll = $this->payrolls->findById($id);
        if ($payroll === null || (string) $payroll['company_id'] !== $companyId) {
            throw new NotFoundException('Planilla de nómina no encontrada');
        }

        $lines = $this->payrolls->findLinesByPayrollId($id);
        return $this->mapPayrollDetail($payroll, $lines);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function createPayroll(string $companyId, array $data): array
    {
        $name = trim((string) ($data['name'] ?? ''));
        $startDate = trim((string) ($data['start_date'] ?? ''));
        $endDate = trim((string) ($data['end_date'] ?? ''));

        if ($name === '') {
            throw new BadRequestException('El nombre de la planilla es obligatorio');
        }
        if ($startDate === '' || $endDate === '') {
            throw new BadRequestException('Las fechas de inicio y fin del periodo son obligatorias');
        }
        if (strtotime($startDate) > strtotime($endDate)) {
            throw new BadRequestException('La fecha de inicio no puede ser posterior a la fecha de fin');
        }

        $id = Database::uuid();

        // Obtener empleados activos con contrato activo
        $activeEmployees = $this->employees->listActiveByCompany($companyId);
        $payrollLines = [];
        $totalEarnings = 0.0;
        $totalDeductions = 0.0;
        $totalNet = 0.0;

        foreach ($activeEmployees as $emp) {
            $baseSalary = (float) ($emp['base_salary'] ?? 0.0);
            $netPay = $baseSalary;

            $payrollLines[] = [
                'id' => Database::uuid(),
                'employee_id' => $emp['id'],
                'base_salary' => $baseSalary,
                'bonuses' => 0.0,
                'deductions' => 0.0,
                'net_pay' => $netPay,
            ];

            $totalEarnings += $baseSalary;
            $totalNet += $netPay;
        }

        $payrollObj = [
            'id' => $id,
            'company_id' => $companyId,
            'name' => $name,
            'start_date' => $startDate,
            'end_date' => $endDate,
            'total_earnings' => $totalEarnings,
            'total_deductions' => $totalDeductions,
            'total_net' => $totalNet,
            'status' => 'draft',
            'journal_entry_id' => null,
        ];

        $this->pdo->beginTransaction();

        try {
            $this->payrolls->insert($payrollObj);
            if ($payrollLines !== []) {
                $this->payrolls->insertLines($id, $payrollLines);
            }
            $this->pdo->commit();
        } catch (\Exception $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }

        return $this->getPayroll($companyId, $id);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function updatePayroll(string $companyId, string $id, array $data): array
    {
        $existing = $this->payrolls->findById($id);
        if ($existing === null || (string) $existing['company_id'] !== $companyId) {
            throw new NotFoundException('Planilla de nómina no encontrada');
        }

        if (in_array((string) $existing['status'], ['posted', 'paid'])) {
            throw new BadRequestException('No se puede modificar una planilla de nómina que ya ha sido pagada o contabilizada');
        }

        $name = trim((string) ($data['name'] ?? $existing['name']));
        $startDate = trim((string) ($data['start_date'] ?? $existing['start_date']));
        $endDate = trim((string) ($data['end_date'] ?? $existing['end_date']));

        if ($name === '') {
            throw new BadRequestException('El nombre de la planilla es obligatorio');
        }
        if ($startDate === '' || $endDate === '') {
            throw new BadRequestException('Las fechas de inicio y fin son obligatorias');
        }
        if (strtotime($startDate) > strtotime($endDate)) {
            throw new BadRequestException('La fecha de inicio no puede ser posterior a la fecha de fin');
        }

        $linesInput = $data['lines'] ?? [];
        $processedLines = [];
        $totalEarnings = 0.0;
        $totalDeductions = 0.0;
        $totalNet = 0.0;

        foreach ($linesInput as $line) {
            $empId = trim((string) ($line['employee_id'] ?? ''));
            $baseSalary = (float) ($line['base_salary'] ?? 0.0);
            $bonuses = (float) ($line['bonuses'] ?? 0.0);
            $deductions = (float) ($line['deductions'] ?? 0.0);

            if ($empId === '') {
                throw new BadRequestException('Identificador de empleado no válido en las líneas');
            }
            if ($baseSalary < 0 || $bonuses < 0 || $deductions < 0) {
                throw new BadRequestException('Los valores salariales, bonificaciones o deducciones no pueden ser negativos');
            }

            $netPay = round($baseSalary + $bonuses - $deductions, 4);
            if ($netPay < 0) {
                throw new BadRequestException('El sueldo neto de un empleado no puede ser negativo');
            }

            $processedLines[] = [
                'id' => Database::uuid(),
                'employee_id' => $empId,
                'base_salary' => $baseSalary,
                'bonuses' => $bonuses,
                'deductions' => $deductions,
                'net_pay' => $netPay,
            ];

            $totalEarnings += ($baseSalary + $bonuses);
            $totalDeductions += $deductions;
            $totalNet += $netPay;
        }

        $payrollObj = [
            'name' => $name,
            'start_date' => $startDate,
            'end_date' => $endDate,
            'total_earnings' => $totalEarnings,
            'total_deductions' => $totalDeductions,
            'total_net' => $totalNet,
            'status' => $existing['status'],
            'journal_entry_id' => $existing['journal_entry_id'] ?: null,
        ];

        $this->pdo->beginTransaction();

        try {
            $this->payrolls->deleteLines($id);
            $this->payrolls->update($id, $payrollObj);
            if ($processedLines !== []) {
                $this->payrolls->insertLines($id, $processedLines);
            }
            $this->pdo->commit();
        } catch (\Exception $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }

        return $this->getPayroll($companyId, $id);
    }

    public function deletePayroll(string $companyId, string $id): void
    {
        $existing = $this->payrolls->findById($id);
        if ($existing === null || (string) $existing['company_id'] !== $companyId) {
            throw new NotFoundException('Planilla de nómina no encontrada');
        }

        if (in_array((string) $existing['status'], ['posted', 'paid'])) {
            throw new BadRequestException('No se puede eliminar una planilla de nómina que ya ha sido pagada o contabilizada');
        }

        $this->pdo->beginTransaction();

        try {
            $this->payrolls->deleteLines($id);
            $this->payrolls->delete($id);
            $this->pdo->commit();
        } catch (\Exception $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }
    }

    /** @param array<string, mixed> $params */
    public function postPayrollToAccounting(string $companyId, ?string $branchId, string $userId, string $id, array $params = []): array
    {
        $payroll = $this->payrolls->findById($id);
        if ($payroll === null || (string) $payroll['company_id'] !== $companyId) {
            throw new NotFoundException('Planilla de nómina no encontrada');
        }

        if ((string) $payroll['status'] === 'posted') {
            throw new BadRequestException('Esta planilla ya ha sido contabilizada');
        }

        $lines = $this->payrolls->findLinesByPayrollId($id);

        $this->pdo->beginTransaction();

        try {
            // 1. Resolver cuentas contables para la contabilización
            $expenseAccountId = trim((string) ($params['expense_account_id'] ?? ''));
            $bankAccountId = trim((string) ($params['bank_account_id'] ?? ''));
            $deductionsAccountId = trim((string) ($params['deductions_account_id'] ?? ''));

            $resolvedAccounts = $this->resolveIntegrationAccounts($companyId, $expenseAccountId, $bankAccountId, $deductionsAccountId);

            // 2. Generar el asiento contable usando el JournalService
            $currencies = $this->journalService->resolveCurrencies($companyId, $branchId, (string) $payroll['end_date']);
            $rate = (float) $currencies['exchange_rate'];
            
            $totalGross = (float) $payroll['total_earnings'];
            $totalDeductions = (float) $payroll['total_deductions'];
            $totalNet = (float) $payroll['total_net'];

            $journalLines = [];
            $lineOrder = 0;

            // Débito: Gasto de salarios (total bruto)
            $journalLines[] = [
                'chart_account_id' => $resolvedAccounts['expense'],
                'line_order' => $lineOrder++,
                'description' => "Gastos de Personal - Planilla #{$payroll['name']}",
                'debit' => $totalGross,
                'credit' => 0.0,
                'debit_functional' => round($totalGross * $rate, 4),
                'credit_functional' => 0.0,
            ];

            // Crédito: Deducciones por pagar (si existen)
            if ($totalDeductions > 0) {
                $journalLines[] = [
                    'chart_account_id' => $resolvedAccounts['deductions'],
                    'line_order' => $lineOrder++,
                    'description' => "Retenciones y Deducciones de Nómina - Planilla #{$payroll['name']}",
                    'debit' => 0.0,
                    'credit' => $totalDeductions,
                    'debit_functional' => 0.0,
                    'credit_functional' => round($totalDeductions * $rate, 4),
                ];
            }

            // Crédito: Bancos (neto pagado a empleados)
            $journalLines[] = [
                'chart_account_id' => $resolvedAccounts['bank'],
                'line_order' => $lineOrder++,
                'description' => "Pago Neto de Salarios a Empleados - Planilla #{$payroll['name']}",
                'debit' => 0.0,
                'credit' => $totalNet,
                'debit_functional' => 0.0,
                'credit_functional' => round($totalNet * $rate, 4),
            ];

            // Crear el asiento contable (se crea en borrador 'draft' y luego se publica)
            $jeData = [
                'entry_date' => $payroll['end_date'],
                'description' => "Contabilización automática de planillas de sueldos: {$payroll['name']}",
                'status' => 'draft',
                'lines' => $journalLines,
            ];

            $journalEntry = $this->journalService->saveEntry($companyId, $branchId, $userId, $jeData);
            
            // Publicar el asiento contable en el libro mayor
            $journalEntry = $this->journalService->postEntry($companyId, (string) $journalEntry['id']);

            // 3. Actualizar estado de la planilla
            $this->payrolls->updateStatus($id, 'posted', (string) $journalEntry['id']);

            $this->pdo->commit();

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

    /** @return array{expense: string, bank: string, deductions: string} */
    private function resolveIntegrationAccounts(string $companyId, string $expenseId, string $bankId, string $deductionsId): array
    {
        $out = [
            'expense' => $expenseId,
            'bank' => $bankId,
            'deductions' => $deductionsId,
        ];

        // Gasto
        if ($out['expense'] === '') {
            $st = $this->pdo->prepare('SELECT id FROM chart_accounts WHERE company_id = ? AND code = \'5105\' LIMIT 1');
            $st->execute([$companyId]);
            $id = $st->fetchColumn();
            if ($id) {
                $out['expense'] = (string) $id;
            } else {
                $st2 = $this->pdo->prepare('SELECT id FROM chart_accounts WHERE company_id = ? AND account_type = \'expense\' LIMIT 1');
                $st2->execute([$companyId]);
                $id2 = $st2->fetchColumn();
                if ($id2) {
                    $out['expense'] = (string) $id2;
                } else {
                    throw new BadRequestException('No se encontró ninguna cuenta de gastos en el plan de cuentas para realizar el asiento.');
                }
            }
        }

        // Banco
        if ($out['bank'] === '') {
            $st = $this->pdo->prepare('SELECT id FROM chart_accounts WHERE company_id = ? AND code = \'1110\' LIMIT 1');
            $st->execute([$companyId]);
            $id = $st->fetchColumn();
            if ($id) {
                $out['bank'] = (string) $id;
            } else {
                $st2 = $this->pdo->prepare('SELECT id FROM chart_accounts WHERE company_id = ? AND account_type = \'asset\' AND is_header = FALSE ORDER BY code ASC LIMIT 1');
                $st2->execute([$companyId]);
                $id2 = $st2->fetchColumn();
                if ($id2) {
                    $out['bank'] = (string) $id2;
                } else {
                    throw new BadRequestException('No se encontró ninguna cuenta de activos para realizar el pago de nómina.');
                }
            }
        }

        // Deducciones / Pasivo
        if ($out['deductions'] === '') {
            $st = $this->pdo->prepare('SELECT id FROM chart_accounts WHERE company_id = ? AND code = \'2105\' LIMIT 1');
            $st->execute([$companyId]);
            $id = $st->fetchColumn();
            if ($id) {
                $out['deductions'] = (string) $id;
            } else {
                $st2 = $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');
                $st2->execute([$companyId]);
                $id2 = $st2->fetchColumn();
                if ($id2) {
                    $out['deductions'] = (string) $id2;
                } else {
                    // Fallback a cuenta de banco/caja si no hay pasivo, pero usualmente hay
                    $out['deductions'] = $out['bank'];
                }
            }
        }

        return $out;
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapPayrollSummary(array $row): array
    {
        return [
            'id' => $row['id'] ?? null,
            'company_id' => $row['company_id'] ?? null,
            'name' => $row['name'] ?? '',
            'start_date' => $row['start_date'] ?? '',
            'end_date' => $row['end_date'] ?? '',
            'total_earnings' => (float) ($row['total_earnings'] ?? 0.0),
            'total_deductions' => (float) ($row['total_deductions'] ?? 0.0),
            'total_net' => (float) ($row['total_net'] ?? 0.0),
            'status' => $row['status'] ?? 'draft',
            'journal_entry_id' => $row['journal_entry_id'] ?? null,
        ];
    }

    /** @param array<string, mixed> $row @param list<array<string, mixed>> $lines @return array<string, mixed> */
    private function mapPayrollDetail(array $row, array $lines): array
    {
        $mappedLines = [];
        foreach ($lines as $line) {
            $mappedLines[] = [
                'id' => $line['id'] ?? null,
                'payroll_id' => $line['payroll_id'] ?? null,
                'employee_id' => $line['employee_id'] ?? null,
                'first_name' => $line['first_name'] ?? '',
                'last_name' => $line['last_name'] ?? '',
                'identification_number' => $line['identification_number'] ?? '',
                'base_salary' => (float) ($line['base_salary'] ?? 0.0),
                'bonuses' => (float) ($line['bonuses'] ?? 0.0),
                'deductions' => (float) ($line['deductions'] ?? 0.0),
                'net_pay' => (float) ($line['net_pay'] ?? 0.0),
            ];
        }

        return [
            'id' => $row['id'] ?? null,
            'company_id' => $row['company_id'] ?? null,
            'name' => $row['name'] ?? '',
            'start_date' => $row['start_date'] ?? '',
            'end_date' => $row['end_date'] ?? '',
            'total_earnings' => (float) ($row['total_earnings'] ?? 0.0),
            'total_deductions' => (float) ($row['total_deductions'] ?? 0.0),
            'total_net' => (float) ($row['total_net'] ?? 0.0),
            'status' => $row['status'] ?? 'draft',
            'journal_entry_id' => $row['journal_entry_id'] ?? null,
            'lines' => $mappedLines,
        ];
    }
}
