<?php

namespace App\Services;

use App\Models\AccountingEntry;
use App\Models\Loan;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;

class AdvancedFinancialReportService
{
    public const REPORTS = [
        'trial-balance' => 'Trial Balance',
        'income-statement' => 'Income Statement',
        'statement-of-financial-position' => 'Statement of Financial Position',
        'portfolio-summary' => 'Portfolio Summary',
    ];

    private const INCOME_CATEGORIES = [
        'interest' => 'Interest Income',
        'fees' => 'Fee Income',
        'penalties' => 'Penalty Income',
        'write_off_recoveries' => 'Write-off Recovery Income',
        'service_income' => 'Service Income',
        'other_income' => 'Other Income',
    ];

    private const EXPENSE_CATEGORIES = [
        'expenses' => 'Operating Expenses',
        'payroll' => 'Payroll Expense',
        'write_offs' => 'Bad Debt / Write-off Expense',
        'other_expenses' => 'Other Expenses',
        'depreciation_expense' => 'Depreciation Expense',
        'asset_maintenance' => 'Fixed Asset Maintenance',
    ];

    public function __construct(
        private MoneyMath $money,
        private BranchContext $branchContext,
    ) {
    }

    public function generate(string $report, array $branchIds, string $from, string $to): array
    {
        if (! isset(self::REPORTS[$report])) {
            throw ValidationException::withMessages(['report' => 'Unknown advanced financial report.']);
        }
        if ($branchIds === []) {
            throw ValidationException::withMessages(['branch_ids' => 'At least one accessible branch is required.']);
        }
        $fromDate = CarbonImmutable::parse($from)->startOfDay();
        $toDate = CarbonImmutable::parse($to)->endOfDay();

        return $this->branchContext->forBranches($branchIds, fn () => match ($report) {
            'trial-balance' => $this->trialBalance($branchIds, $fromDate, $toDate),
            'income-statement' => $this->incomeStatement($branchIds, $fromDate, $toDate),
            'statement-of-financial-position' => $this->financialPosition($branchIds, $toDate),
            'portfolio-summary' => $this->portfolioSummary($branchIds, $toDate),
        });
    }

    private function trialBalance(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $entries = AccountingEntry::with(['financialAccount.branch'])
            ->whereIn('branch_id', $branchIds)
            ->where('entry_date', '<=', $to)
            ->orderBy('entry_date')->orderBy('id')->get();

        $rows = $entries->groupBy(fn (AccountingEntry $entry) => $this->ledgerKey($entry))
            ->map(function (Collection $group) use ($from): array {
                $sample = $group->first();
                $opening = $group->filter(fn (AccountingEntry $entry) => $entry->entry_date->lt($from));
                $period = $group->filter(fn (AccountingEntry $entry) => $entry->entry_date->gte($from));
                $openingMinor = $this->debits($opening) - $this->credits($opening);
                $periodDebit = $this->debits($period);
                $periodCredit = $this->credits($period);
                $closingMinor = $openingMinor + $periodDebit - $periodCredit;
                return [
                    'label' => $this->ledgerLabel($sample),
                    'category' => $sample->category,
                    'opening_balance' => $this->money->fromSignedMinor($openingMinor),
                    'period_debit' => $this->money->fromMinor($periodDebit),
                    'period_credit' => $this->money->fromMinor($periodCredit),
                    'debit_balance' => $this->money->fromMinor(max(0, $closingMinor)),
                    'credit_balance' => $this->money->fromMinor(max(0, -$closingMinor)),
                    'closing_minor' => $closingMinor,
                ];
            })->sortBy('label')->values();

        $periodDebits = $rows->sum(fn ($row) => $this->money->toMinor($row['period_debit']));
        $periodCredits = $rows->sum(fn ($row) => $this->money->toMinor($row['period_credit']));
        $debitBalances = $rows->sum(fn ($row) => $this->money->toMinor($row['debit_balance']));
        $creditBalances = $rows->sum(fn ($row) => $this->money->toMinor($row['credit_balance']));

        return [
            'rows' => $rows,
            'period_debits' => $this->money->fromMinor($periodDebits),
            'period_credits' => $this->money->fromMinor($periodCredits),
            'debit_balances' => $this->money->fromMinor($debitBalances),
            'credit_balances' => $this->money->fromMinor($creditBalances),
            'difference' => $this->money->fromSignedMinor($debitBalances - $creditBalances),
            'balanced' => $debitBalances === $creditBalances,
        ];
    }

    private function incomeStatement(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $entries = AccountingEntry::whereIn('branch_id', $branchIds)
            ->whereBetween('entry_date', [$from, $to])->get();
        $incomeRows = collect(self::INCOME_CATEGORIES)->map(function (string $label, string $category) use ($entries): array {
            $amount = $this->categoryNetCredit($entries, [$category]);
            return ['category' => $category, 'label' => $label, 'amount' => $this->money->fromSignedMinor($amount), 'amount_minor' => $amount];
        })->values();
        $expenseRows = collect(self::EXPENSE_CATEGORIES)->map(function (string $label, string $category) use ($entries): array {
            $amount = $this->categoryNetDebit($entries, [$category]);
            return ['category' => $category, 'label' => $label, 'amount' => $this->money->fromSignedMinor($amount), 'amount_minor' => $amount];
        })->values();

        $settlement = $this->categoryNetCredit($entries, ['settlement_adjustment']);
        if ($settlement >= 0) {
            $incomeRows->push(['category' => 'settlement_adjustment','label' => 'Settlement Adjustments','amount' => $this->money->fromMinor($settlement),'amount_minor' => $settlement]);
        } else {
            $expenseRows->push(['category' => 'settlement_adjustment','label' => 'Settlement Adjustments','amount' => $this->money->fromMinor(abs($settlement)),'amount_minor' => abs($settlement)]);
        }

        $incomeMinor = $incomeRows->sum('amount_minor');
        $expenseMinor = $expenseRows->sum('amount_minor');
        return [
            'income_rows' => $incomeRows,
            'expense_rows' => $expenseRows,
            'total_income' => $this->money->fromSignedMinor($incomeMinor),
            'total_expenses' => $this->money->fromSignedMinor($expenseMinor),
            'net_profit' => $this->money->fromSignedMinor($incomeMinor - $expenseMinor),
            'entry_count' => $entries->count(),
        ];
    }

    private function financialPosition(array $branchIds, CarbonImmutable $to): array
    {
        $entries = AccountingEntry::whereIn('branch_id', $branchIds)->where('entry_date', '<=', $to)->get();
        $financialAssets = $this->categoryNetDebit($entries, ['assets']);
        $principalReceivable = $this->categoryNetDebit($entries, ['principal']);
        $fixedAssets = $this->categoryNetDebit($entries, ['fixed_assets']);
        $accumulatedDepreciation = $this->categoryNetCredit($entries, ['accumulated_depreciation']);
        $capital = $this->categoryNetCredit($entries, ['capital']);
        $income = $this->categoryNetCredit($entries, array_keys(self::INCOME_CATEGORIES));
        $expenses = $this->categoryNetDebit($entries, array_keys(self::EXPENSE_CATEGORIES));
        $settlement = $this->categoryNetCredit($entries, ['settlement_adjustment']);
        $cumulativeResult = $income - $expenses + $settlement;
        $totalAssets = $financialAssets + $principalReceivable + $fixedAssets - $accumulatedDepreciation;
        $liabilities = 0;
        $equity = $capital + $cumulativeResult;
        $difference = $totalAssets - $liabilities - $equity;

        return [
            'asset_rows' => [
                ['label' => 'Financial Accounts / Cash & Bank','amount' => $this->money->fromSignedMinor($financialAssets)],
                ['label' => 'Loan Principal Receivable','amount' => $this->money->fromSignedMinor($principalReceivable)],
                ['label' => 'Fixed Assets at Cost','amount' => $this->money->fromSignedMinor($fixedAssets)],
                ['label' => 'Less: Accumulated Depreciation','amount' => $this->money->fromSignedMinor(-$accumulatedDepreciation)],
            ],
            'equity_rows' => [
                ['label' => 'Capital','amount' => $this->money->fromSignedMinor($capital)],
                ['label' => 'Cumulative Profit / Loss','amount' => $this->money->fromSignedMinor($cumulativeResult)],
            ],
            'total_assets' => $this->money->fromSignedMinor($totalAssets),
            'total_liabilities' => $this->money->fromMinor($liabilities),
            'total_equity' => $this->money->fromSignedMinor($equity),
            'difference' => $this->money->fromSignedMinor($difference),
            'balanced' => $difference === 0,
            'as_of' => $to->toDateString(),
            'liability_note' => 'No standalone liability ledger category exists yet; liabilities remain zero until liability accounting is introduced.',
        ];
    }

    private function portfolioSummary(array $branchIds, CarbonImmutable $to): array
    {
        $loans = Loan::with('branch')->whereIn('branch_id', $branchIds)->where('status', 'active')->get();
        $writtenOff = Loan::whereIn('branch_id', $branchIds)->where('status', 'written_off')->count();
        $sum = fn (Collection $items, string $field): int => $items->sum(fn (Loan $loan) => $this->money->toMinor((string) ($loan->{$field} ?? '0')));
        $principal = $sum($loans, 'principal_outstanding');
        $interest = $sum($loans, 'interest_outstanding');
        $fees = $sum($loans, 'fees_outstanding');
        $penalties = $sum($loans, 'penalties_outstanding');
        $outstanding = $sum($loans, 'total_outstanding');
        $overdue = $sum($loans, 'total_overdue');
        $overduePrincipal = $sum($loans, 'overdue_principal');

        $branchRows = $loans->groupBy('branch_id')->map(function (Collection $items) use ($sum): array {
            $principal = $sum($items, 'principal_outstanding');
            $overduePrincipal = $sum($items, 'overdue_principal');
            return [
                'branch' => $items->first()->branch,
                'active_loans' => $items->count(),
                'principal_outstanding' => $this->money->fromMinor($principal),
                'total_outstanding' => $this->money->fromMinor($sum($items, 'total_outstanding')),
                'overdue' => $this->money->fromMinor($sum($items, 'total_overdue')),
                'par_percent' => $this->percentage($overduePrincipal, $principal),
            ];
        })->sortBy(fn ($row) => $row['branch']->name)->values();

        return [
            'active_loans' => $loans->count(),
            'overdue_loans' => $loans->filter(fn (Loan $loan) => $this->money->toMinor((string) ($loan->total_overdue ?? '0')) > 0)->count(),
            'written_off_loans' => $writtenOff,
            'principal_outstanding' => $this->money->fromMinor($principal),
            'interest_outstanding' => $this->money->fromMinor($interest),
            'fees_outstanding' => $this->money->fromMinor($fees),
            'penalties_outstanding' => $this->money->fromMinor($penalties),
            'total_outstanding' => $this->money->fromMinor($outstanding),
            'total_overdue' => $this->money->fromMinor($overdue),
            'portfolio_at_risk_percent' => $this->percentage($overduePrincipal, $principal),
            'branch_rows' => $branchRows,
            'snapshot_date' => now()->toDateString(),
            'requested_as_of' => $to->toDateString(),
            'historical_note' => 'Portfolio balances are operational current snapshots. Historical as-of reconstruction will require balance snapshots in a later reporting enhancement.',
        ];
    }

    private function ledgerKey(AccountingEntry $entry): string
    {
        if ($entry->category === 'assets' && $entry->financial_account_id) return 'account:'.$entry->financial_account_id;
        return 'category:'.$entry->category;
    }

    private function ledgerLabel(AccountingEntry $entry): string
    {
        if ($entry->category === 'assets' && $entry->financialAccount) {
            return ($entry->financialAccount->branch?->name ? $entry->financialAccount->branch->name.' · ' : '').$entry->financialAccount->name;
        }
        return match ($entry->category) {
            'principal' => 'Loan Principal Receivable',
            'capital' => 'Capital',
            'fixed_assets' => 'Fixed Assets at Cost',
            'accumulated_depreciation' => 'Accumulated Depreciation',
            'depreciation_expense' => 'Depreciation Expense',
            'asset_maintenance' => 'Fixed Asset Maintenance',
            default => str($entry->category)->replace('_', ' ')->title()->toString(),
        };
    }

    private function categoryNetCredit(Collection $entries, array $categories): int
    {
        $items = $entries->whereIn('category', $categories);
        return $this->credits($items) - $this->debits($items);
    }

    private function categoryNetDebit(Collection $entries, array $categories): int
    {
        $items = $entries->whereIn('category', $categories);
        return $this->debits($items) - $this->credits($items);
    }

    private function debits(Collection $entries): int
    {
        return $entries->sum(fn (AccountingEntry $entry) => $this->money->toMinor((string) $entry->debit));
    }

    private function credits(Collection $entries): int
    {
        return $entries->sum(fn (AccountingEntry $entry) => $this->money->toMinor((string) $entry->credit));
    }

    private function percentage(int $numerator, int $denominator): string
    {
        if ($denominator <= 0) return '0.00';
        return number_format(($numerator * 100) / $denominator, 2, '.', '');
    }
}
