<?php

namespace App\Services;

use App\Models\Employee;
use App\Models\PayrollEntry;
use App\Models\PayrollRun;
use App\Models\SalaryComponent;
use Carbon\Carbon;
use Illuminate\Support\Collection;

class TaxReportService
{
    public function generate(array $filters): array
    {
        $filters = $this->normalizeFilters($filters);
        $companyIds = getCompanyAndUsersId();

        $runsQuery = PayrollRun::query()
            ->whereIn('created_by', $companyIds)
            ->where('status', 'completed');

        $this->applyDateFilter($runsQuery, $filters, 'pay_period_end');

        if (empty($filters['start_date']) && empty($filters['end_date'])) {
            $runsQuery->whereYear('pay_period_end', Carbon::now()->year);
        }

        $runIds = $runsQuery->pluck('id')->all();

        $entriesQuery = PayrollEntry::query()
            ->whereIn('created_by', $companyIds)
            ->whereIn('payroll_run_id', $runIds)
            ->with([
                'payrollRun:id,title,pay_period_start,pay_period_end,pay_date',
                'employee:id,name',
                'employee.employee:id,user_id,tax_payer_id,branch_id,department_id,employee_id',
                'employee.employee.branch:id,name',
                'employee.employee.department:id,name',
            ])
            ->orderByDesc('payroll_run_id')
            ->orderBy('employee_id');

        $this->applyEmployeeIdFilter($entriesQuery, $filters, 'employee_id');

        $entries = $entriesQuery->get();
        $taxableByComponent = $this->taxableComponentMap($companyIds);

        $totalPaye = 0.0;
        $totalTaxableIncome = 0.0;
        $totalStatutory = 0.0;
        $totalGross = 0.0;
        $missingTpin = 0;
        $employeesSeen = [];

        $detailRows = [];
        $byDepartment = [];
        $byPeriod = [];
        $byBranch = [];
        $statutoryTotals = [];

        foreach ($entries as $entry) {
            $profile = $entry->employee?->employee;
            $earnings = $this->normalizeBreakdown($entry->earnings_breakdown);
            $deductions = $this->normalizeBreakdown($entry->deductions_breakdown);

            $taxableIncome = $this->calculateTaxableIncome($entry, $earnings, $taxableByComponent);
            $paye = $this->sumMatchingDeductions($deductions, 'income_tax');
            $statutory = $this->sumMatchingDeductions($deductions, 'statutory');
            $otherDeductions = max(0, (float) $entry->total_deductions - $paye - $statutory);

            $totalPaye += $paye;
            $totalTaxableIncome += $taxableIncome;
            $totalStatutory += $statutory;
            $totalGross += (float) $entry->gross_pay;
            $employeesSeen[$entry->employee_id] = true;

            $tpin = trim((string) ($profile?->tax_payer_id ?? ''));
            if ($tpin === '') {
                $missingTpin++;
            }

            $department = $profile?->department?->name ?? 'Unassigned';
            $branch = $profile?->branch?->name ?? 'Unassigned';
            $periodLabel = $this->periodLabel($entry->payrollRun);

            $byDepartment[$department] = ($byDepartment[$department] ?? 0) + $paye;
            $byBranch[$branch] = ($byBranch[$branch] ?? 0) + $paye;
            $byPeriod[$periodLabel] = ($byPeriod[$periodLabel] ?? 0) + $paye;

            foreach ($deductions as $name => $amount) {
                if ($this->deductionMatches($name, 'statutory') && (float) $amount > 0) {
                    $statutoryTotals[$name] = ($statutoryTotals[$name] ?? 0) + (float) $amount;
                }
            }

            $detailRows[] = [
                $periodLabel,
                $entry->employee?->name ?? '—',
                $tpin !== '' ? $tpin : '—',
                $profile?->employee_id ?? '—',
                $branch,
                $department,
                formatCurrency($taxableIncome),
                formatCurrency($paye),
                formatCurrency($statutory),
                $otherDeductions > 0 ? formatCurrency($otherDeductions) : '—',
                formatCurrency((float) $entry->gross_pay),
                formatCurrency((float) $entry->net_pay),
            ];
        }

        $payrollPeriodCount = count(array_unique($entries->pluck('payroll_run_id')->all()));

        $missingTpinRows = $this->missingTpinRows($companyIds, $filters, array_keys($employeesSeen));

        return [
            'summary' => [
                ['label' => 'Payroll Periods', 'value' => $payrollPeriodCount, 'format' => 'number'],
                ['label' => 'Employees in Report', 'value' => count($employeesSeen), 'format' => 'number'],
                ['label' => 'Payroll Entries', 'value' => $entries->count(), 'format' => 'number'],
                ['label' => 'Total Taxable Income', 'value' => $totalTaxableIncome, 'format' => 'currency'],
                ['label' => 'Total PAYE Withheld', 'value' => $totalPaye, 'format' => 'currency'],
                ['label' => 'Total Statutory Deductions', 'value' => $totalStatutory, 'format' => 'currency'],
                ['label' => 'Total Gross Pay', 'value' => $totalGross, 'format' => 'currency'],
                ['label' => 'Missing Tax Payer ID (entries)', 'value' => $missingTpin, 'format' => 'number'],
            ],
            'tables' => [
                [
                    'title' => 'Employee Tax Withholding Detail',
                    'columns' => [
                        'Pay Period',
                        'Employee',
                        'Tax Payer ID (TPIN)',
                        'Employee No.',
                        'Branch',
                        'Department',
                        'Taxable Income',
                        'PAYE',
                        'Statutory',
                        'Other Deductions',
                        'Gross Pay',
                        'Net Pay',
                    ],
                    'rows' => $detailRows,
                ],
                [
                    'title' => 'PAYE by Department',
                    'columns' => ['Department', 'PAYE Withheld'],
                    'rows' => $this->sortAmountRows($byDepartment),
                ],
                [
                    'title' => 'PAYE by Branch',
                    'columns' => ['Branch', 'PAYE Withheld'],
                    'rows' => $this->sortAmountRows($byBranch),
                ],
                [
                    'title' => 'PAYE by Pay Period',
                    'columns' => ['Pay Period', 'PAYE Withheld'],
                    'rows' => $this->sortPeriodRows($byPeriod),
                ],
                [
                    'title' => 'Statutory Deductions Summary',
                    'columns' => ['Deduction', 'Total Amount'],
                    'rows' => $this->sortAmountRows($statutoryTotals),
                ],
                [
                    'title' => 'Employees Missing Tax Payer ID',
                    'columns' => ['Employee', 'Employee No.', 'Branch', 'Department', 'Status'],
                    'rows' => $missingTpinRows,
                ],
            ],
        ];
    }

    private function normalizeFilters(array $filters): array
    {
        return [
            'branch_id' => ! empty($filters['branch_id']) ? (int) $filters['branch_id'] : null,
            'employee_id' => ! empty($filters['employee_id']) ? (int) $filters['employee_id'] : null,
            'start_date' => $this->normalizeFilterDate($filters['start_date'] ?? null),
            'end_date' => $this->normalizeFilterDate($filters['end_date'] ?? null),
        ];
    }

    private function normalizeFilterDate(mixed $value): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }

        try {
            return Carbon::parse($value)->toDateString();
        } catch (\Throwable) {
            return null;
        }
    }

    private function applyDateFilter($query, array $filters, string $column): void
    {
        if (! empty($filters['start_date'])) {
            $query->whereDate($column, '>=', $filters['start_date']);
        }

        if (! empty($filters['end_date'])) {
            $query->whereDate($column, '<=', $filters['end_date']);
        }
    }

    private function applyEmployeeIdFilter($query, array $filters, string $column = 'employee_id'): void
    {
        $employeeIds = $this->filteredEmployeeIds($filters);
        if ($employeeIds === null) {
            return;
        }

        if ($employeeIds === []) {
            $query->whereRaw('1 = 0');

            return;
        }

        $query->whereIn($column, $employeeIds);
    }

    private function filteredEmployeeIds(array $filters): ?array
    {
        if (empty($filters['branch_id']) && empty($filters['employee_id'])) {
            return null;
        }

        return Employee::whereIn('created_by', getCompanyAndUsersId())
            ->when(! empty($filters['branch_id']), fn ($query) => $query->where('branch_id', $filters['branch_id']))
            ->when(! empty($filters['employee_id']), fn ($query) => $query->where('user_id', $filters['employee_id']))
            ->pluck('user_id')
            ->map(fn ($id) => (int) $id)
            ->all();
    }

    private function taxableComponentMap(array $companyIds): Collection
    {
        return SalaryComponent::query()
            ->whereIn('created_by', $companyIds)
            ->where('status', 'active')
            ->get(['name', 'is_taxable', 'type'])
            ->keyBy('name');
    }

    private function normalizeBreakdown(mixed $breakdown): array
    {
        if (is_array($breakdown)) {
            return $breakdown;
        }

        if (is_string($breakdown) && $breakdown !== '') {
            $decoded = json_decode($breakdown, true);

            return is_array($decoded) ? $decoded : [];
        }

        return [];
    }

    private function calculateTaxableIncome(PayrollEntry $entry, array $earnings, Collection $taxableByComponent): float
    {
        $taxable = 0.0;
        $defaultTaxableNames = config('hr_tax_report.default_taxable_earning_names', ['Basic Salary']);

        foreach ($earnings as $name => $amount) {
            $amount = (float) $amount;
            if ($amount <= 0) {
                continue;
            }

            $component = $taxableByComponent->get($name);
            $isTaxable = $component
                ? (bool) $component->is_taxable
                : in_array($name, $defaultTaxableNames, true);

            if ($isTaxable) {
                $taxable += $amount;
            }
        }

        if ($taxable <= 0) {
            $taxable = (float) $entry->basic_salary;
        }

        if ((float) $entry->overtime_amount > 0) {
            $taxable += (float) $entry->overtime_amount;
        }

        return round($taxable, 2);
    }

    private function sumMatchingDeductions(array $deductions, string $type): float
    {
        $total = 0.0;

        foreach ($deductions as $name => $amount) {
            if ($this->deductionMatches($name, $type)) {
                $total += (float) $amount;
            }
        }

        return round($total, 2);
    }

    private function deductionMatches(string $name, string $type): bool
    {
        $normalized = strtolower(trim($name));

        if ($type === 'income_tax') {
            foreach (config('hr_tax_report.income_tax_deduction_names', []) as $exact) {
                if (strcasecmp($name, $exact) === 0) {
                    return true;
                }
            }

            foreach (config('hr_tax_report.income_tax_deduction_patterns', []) as $pattern) {
                if (str_contains($normalized, strtolower($pattern))) {
                    return true;
                }
            }

            return false;
        }

        if ($type === 'statutory') {
            if ($this->deductionMatches($name, 'income_tax')) {
                return false;
            }

            foreach (config('hr_tax_report.statutory_deduction_patterns', []) as $pattern) {
                if (str_contains($normalized, strtolower($pattern))) {
                    return true;
                }
            }
        }

        return false;
    }

    private function periodLabel(?PayrollRun $run): string
    {
        if (! $run) {
            return '—';
        }

        if ($run->title) {
            return $run->title;
        }

        $start = $run->pay_period_start?->format('M Y') ?? '';
        $end = $run->pay_period_end?->format('M Y') ?? '';

        if ($start !== '' && $end !== '' && $start !== $end) {
            return "{$start} – {$end}";
        }

        return $start ?: ($run->pay_date?->format('M Y') ?? '—');
    }

    private function sortAmountRows(array $rows): array
    {
        arsort($rows);

        return collect($rows)
            ->map(fn ($amount, $label) => [$label, formatCurrency((float) $amount)])
            ->values()
            ->all();
    }

    private function sortPeriodRows(array $rows): array
    {
        ksort($rows);

        return collect($rows)
            ->map(fn ($amount, $label) => [$label, formatCurrency((float) $amount)])
            ->values()
            ->all();
    }

    private function missingTpinRows(array $companyIds, array $filters, array $includedUserIds): array
    {
        $query = Employee::query()
            ->whereIn('created_by', $companyIds)
            ->whereIn('employee_status', ['active', 'probation'])
            ->where(function ($q) {
                $q->whereNull('tax_payer_id')->orWhere('tax_payer_id', '');
            })
            ->with(['user:id,name', 'branch:id,name', 'department:id,name']);

        if (! empty($filters['branch_id'])) {
            $query->where('branch_id', $filters['branch_id']);
        }

        if (! empty($filters['employee_id'])) {
            $query->where('user_id', $filters['employee_id']);
        }

        return $query
            ->orderBy('employee_id')
            ->get()
            ->map(fn (Employee $employee) => [
                $employee->user?->name ?? '—',
                $employee->employee_id ?? '—',
                $employee->branch?->name ?? 'Unassigned',
                $employee->department?->name ?? 'Unassigned',
                in_array($employee->user_id, $includedUserIds, true) ? 'In payroll report' : 'Not in selected period',
            ])
            ->values()
            ->all();
    }
}
