<?php

namespace App\Services;

use App\Models\AccountingEntry;
use App\Models\Expense;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\PayrollEntry;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;

class FinancialReportService
{
    public const REPORTS = [
        'profit-loss' => 'Profit & Loss',
        'cash-flow' => 'Cash Flow',
        'account-statement' => 'Account Statement',
        'capital' => 'Capital Report',
        'expenses' => 'Expense Report',
        'payroll' => 'Payroll Report',
        'collections' => 'Collection Report',
        'disbursements' => 'Disbursement Report',
    ];

    private const COLLECTION_TYPES = ['loan_payment', 'loan_early_settlement', 'loan_write_off_recovery'];
    private const DISBURSEMENT_TYPES = ['loan_disbursement', 'loan_top_up'];
    private const EXPENSE_TYPES = ['expense', 'payroll'];
    private const CAPITAL_TYPES = ['opening_balance', 'capital_injection'];

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

    public function generate(string $report, array $branchIds, string $from, string $to, ?int $accountId = null): array
    {
        if (! array_key_exists($report, self::REPORTS)) {
            throw ValidationException::withMessages(['report' => 'Unknown 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();
        if ($toDate->lt($fromDate)) {
            throw ValidationException::withMessages(['to' => 'The report end date cannot be before the start date.']);
        }

        return $this->branchContext->forBranches($branchIds, fn () => match ($report) {
            'profit-loss' => $this->profitAndLoss($branchIds, $fromDate, $toDate),
            'cash-flow' => $this->cashFlow($branchIds, $fromDate, $toDate),
            'account-statement' => $this->accountStatement($branchIds, $fromDate, $toDate, $accountId),
            'capital' => $this->capitalReport($branchIds, $fromDate, $toDate),
            'expenses' => $this->expenseReport($branchIds, $fromDate, $toDate),
            'payroll' => $this->payrollReport($branchIds, $fromDate, $toDate),
            'collections' => $this->collectionReport($branchIds, $fromDate, $toDate),
            'disbursements' => $this->disbursementReport($branchIds, $fromDate, $toDate),
        });
    }

    private function profitAndLoss(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $entries = AccountingEntry::query()
            ->with('branch')
            ->whereIn('branch_id', $branchIds)
            ->whereBetween('entry_date', [$from, $to])
            ->orderBy('entry_date')
            ->orderBy('id')
            ->get();

        $incomeRows = [
            $this->amountRow('Interest income', 'interest', $this->credits($entries, ['interest'])),
            $this->amountRow('Fee income', 'fees', $this->credits($entries, ['fees'])),
            $this->amountRow('Penalty income', 'penalties', $this->credits($entries, ['penalties'])),
            $this->amountRow('Written-off loan recovery income', 'write_off_recoveries', $this->credits($entries, ['write_off_recoveries'])),
            $this->amountRow('Service income', 'service_income', $this->credits($entries, ['service_income'])),
            $this->amountRow('Other actual income', 'other_income', $this->credits($entries, ['other_income', 'settlement_adjustment'])),
        ];
        $expenseRows = [
            $this->amountRow('Operating expenses', 'expenses', $this->debits($entries, ['expenses'])),
            $this->amountRow('Payroll expense', 'payroll', $this->debits($entries, ['payroll'])),
            $this->amountRow('Bad-debt / write-off expense', 'write_offs', $this->debits($entries, ['write_offs'])),
            $this->amountRow('Depreciation expense', 'depreciation_expense', $this->debits($entries, ['depreciation_expense'])),
            $this->amountRow('Fixed asset maintenance', 'asset_maintenance', $this->debits($entries, ['asset_maintenance'])),
            $this->amountRow('Settlement adjustments / other expense', 'other_expenses', $this->debits($entries, ['settlement_adjustment', 'other_expenses'])),
        ];

        $incomeMinor = collect($incomeRows)->sum('amount_minor');
        $expenseMinor = collect($expenseRows)->sum('amount_minor');
        $principalExcluded = $this->credits($entries, ['principal']);

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

    private function cashFlow(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $transactions = $this->transactionQuery($branchIds, $from, $to)
            ->with(['branch', 'account'])
            ->get();

        $rows = $transactions->groupBy('type')->map(function (Collection $items, string $type): array {
            $in = $this->fieldMinor($items, 'money_in');
            $out = $this->fieldMinor($items, 'money_out');
            return [
                'type' => $type,
                'label' => $this->transactionTypeLabel($type),
                'count' => $items->count(),
                'money_in' => $this->money->fromMinor($in),
                'money_out' => $this->money->fromMinor($out),
                'net' => $this->money->fromSignedMinor($in - $out),
                'money_in_minor' => $in,
                'money_out_minor' => $out,
            ];
        })->sortBy('label')->values();

        $moneyIn = $this->fieldMinor($transactions, 'money_in');
        $moneyOut = $this->fieldMinor($transactions, 'money_out');
        $operatingIn = $this->transactionTypeMinor($transactions, self::COLLECTION_TYPES, 'money_in');
        $operatingOut = $this->transactionTypeMinor($transactions, self::EXPENSE_TYPES, 'money_out');
        $lendingOut = $this->transactionTypeMinor($transactions, self::DISBURSEMENT_TYPES, 'money_out');
        $capitalIn = $this->transactionTypeMinor($transactions, self::CAPITAL_TYPES, 'money_in');
        $transferIn = $this->transactionTypeMinor($transactions, ['transfer_in'], 'money_in');
        $transferOut = $this->transactionTypeMinor($transactions, ['transfer_out'], 'money_out');

        return [
            'rows' => $rows,
            'money_in' => $this->money->fromMinor($moneyIn),
            'money_out' => $this->money->fromMinor($moneyOut),
            'net_cash_flow' => $this->money->fromSignedMinor($moneyIn - $moneyOut),
            'collections_in' => $this->money->fromMinor($operatingIn),
            'operating_out' => $this->money->fromMinor($operatingOut),
            'loan_disbursements_out' => $this->money->fromMinor($lendingOut),
            'capital_in' => $this->money->fromMinor($capitalIn),
            'net_transfers' => $this->money->fromSignedMinor($transferIn - $transferOut),
            'transaction_count' => $transactions->count(),
        ];
    }

    private function accountStatement(array $branchIds, CarbonImmutable $from, CarbonImmutable $to, ?int $accountId): array
    {
        $accountsQuery = FinancialAccount::query()->with('branch')->whereIn('branch_id', $branchIds)->orderBy('branch_id')->orderBy('name');
        if ($accountId) {
            $accountsQuery->whereKey($accountId);
        }
        $accounts = $accountsQuery->get();
        if ($accountId && $accounts->isEmpty()) {
            throw ValidationException::withMessages(['account_id' => 'The selected account is not available in the selected branch scope.']);
        }

        $summaries = [];
        $detailRows = [];
        foreach ($accounts as $account) {
            $openingTransactions = FinancialAccountTransaction::query()
                ->where('account_id', $account->id)
                ->where('transaction_date', '<', $from)
                ->get(['money_in', 'money_out']);
            $openingMinor = $this->fieldMinor($openingTransactions, 'money_in') - $this->fieldMinor($openingTransactions, 'money_out');

            $periodTransactions = FinancialAccountTransaction::query()
                ->with(['branch', 'account', 'user'])
                ->where('account_id', $account->id)
                ->whereBetween('transaction_date', [$from, $to])
                ->orderBy('transaction_date')
                ->orderBy('id')
                ->get();
            $moneyIn = $this->fieldMinor($periodTransactions, 'money_in');
            $moneyOut = $this->fieldMinor($periodTransactions, 'money_out');
            $closingMinor = $openingMinor + $moneyIn - $moneyOut;

            $summaries[] = [
                'account' => $account,
                'opening_balance' => $this->money->fromSignedMinor($openingMinor),
                'money_in' => $this->money->fromMinor($moneyIn),
                'money_out' => $this->money->fromMinor($moneyOut),
                'closing_balance' => $this->money->fromSignedMinor($closingMinor),
                'transaction_count' => $periodTransactions->count(),
            ];

            if ($accountId) {
                $running = $openingMinor;
                foreach ($periodTransactions as $transaction) {
                    $running += $this->money->toMinor((string) $transaction->money_in);
                    $running -= $this->money->toMinor((string) $transaction->money_out);
                    $detailRows[] = [
                        'transaction' => $transaction,
                        'report_balance' => $this->money->fromSignedMinor($running),
                    ];
                }
            }
        }

        return [
            'summaries' => $summaries,
            'rows' => $detailRows,
            'selected_account_id' => $accountId,
        ];
    }

    private function capitalReport(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $rows = $this->transactionQuery($branchIds, $from, $to)
            ->whereIn('type', self::CAPITAL_TYPES)
            ->with(['branch', 'account', 'user'])
            ->get();
        $total = $this->fieldMinor($rows, 'money_in');

        return [
            'rows' => $rows,
            'total_capital' => $this->money->fromMinor($total),
            'opening_funds' => $this->money->fromMinor($this->transactionTypeMinor($rows, ['opening_balance'], 'money_in')),
            'capital_injections' => $this->money->fromMinor($this->transactionTypeMinor($rows, ['capital_injection'], 'money_in')),
        ];
    }

    private function expenseReport(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $rows = Expense::query()
            ->with(['branch', 'category', 'financialAccount', 'enteredBy', 'approver'])
            ->whereIn('branch_id', $branchIds)
            ->where('status', 'approved')
            ->whereDate('expense_date', '>=', $from->toDateString())
            ->whereDate('expense_date', '<=', $to->toDateString())
            ->orderBy('expense_date')
            ->orderBy('id')
            ->get();
        $totalMinor = $this->fieldMinor($rows, 'amount');
        $byCategory = $rows->groupBy(fn (Expense $expense) => $expense->category?->name ?? 'Uncategorized')
            ->map(fn (Collection $items, string $label) => [
                'label' => $label,
                'count' => $items->count(),
                'amount' => $this->money->fromMinor($this->fieldMinor($items, 'amount')),
            ])->sortBy('label')->values();

        return ['rows' => $rows, 'by_category' => $byCategory, 'total_expenses' => $this->money->fromMinor($totalMinor)];
    }

    private function payrollReport(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $rows = PayrollEntry::query()
            ->with(['period', 'branch', 'financialAccount', 'accountTransaction', 'payer'])
            ->whereIn('branch_id', $branchIds)
            ->where('status', 'paid')
            ->whereBetween('paid_at', [$from, $to])
            ->orderBy('paid_at')
            ->orderBy('id')
            ->get();

        return [
            'rows' => $rows,
            'total_gross' => $this->money->fromMinor($this->fieldMinor($rows, 'gross')),
            'total_deductions' => $this->money->fromMinor($this->fieldMinor($rows, 'total_deductions')),
            'total_net' => $this->money->fromMinor($this->fieldMinor($rows, 'net')),
        ];
    }

    private function collectionReport(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $rows = $this->transactionQuery($branchIds, $from, $to)
            ->whereIn('type', self::COLLECTION_TYPES)
            ->with(['branch', 'account', 'user'])
            ->get();
        $total = $this->fieldMinor($rows, 'money_in');
        $channels = $rows->groupBy(fn (FinancialAccountTransaction $transaction) => $transaction->account?->account_type ?? 'other')
            ->map(fn (Collection $items, string $channel) => [
                'channel' => $channel,
                'label' => str($channel)->replace('_', ' ')->title()->toString(),
                'count' => $items->count(),
                'amount' => $this->money->fromMinor($this->fieldMinor($items, 'money_in')),
            ])->sortBy('label')->values();

        return ['rows' => $rows, 'channels' => $channels, 'total_collections' => $this->money->fromMinor($total)];
    }

    private function disbursementReport(array $branchIds, CarbonImmutable $from, CarbonImmutable $to): array
    {
        $rows = $this->transactionQuery($branchIds, $from, $to)
            ->whereIn('type', self::DISBURSEMENT_TYPES)
            ->with(['branch', 'account', 'user'])
            ->get();
        $total = $this->fieldMinor($rows, 'money_out');
        $byType = $rows->groupBy('type')->map(fn (Collection $items, string $type) => [
            'type' => $type,
            'label' => $this->transactionTypeLabel($type),
            'count' => $items->count(),
            'amount' => $this->money->fromMinor($this->fieldMinor($items, 'money_out')),
        ])->sortBy('label')->values();

        return ['rows' => $rows, 'by_type' => $byType, 'total_disbursements' => $this->money->fromMinor($total)];
    }

    private function transactionQuery(array $branchIds, CarbonImmutable $from, CarbonImmutable $to)
    {
        return FinancialAccountTransaction::query()
            ->whereIn('branch_id', $branchIds)
            ->whereBetween('transaction_date', [$from, $to])
            ->orderBy('transaction_date')
            ->orderBy('id');
    }

    private function credits(Collection $entries, array $categories): int
    {
        return $this->fieldMinor($entries->whereIn('category', $categories), 'credit');
    }

    private function debits(Collection $entries, array $categories): int
    {
        return $this->fieldMinor($entries->whereIn('category', $categories), 'debit');
    }

    private function fieldMinor(Collection $items, string $field): int
    {
        $total = 0;
        foreach ($items as $item) {
            $value = is_array($item) ? ($item[$field] ?? '0') : ($item->{$field} ?? '0');
            $total += $this->money->toMinor((string) $value);
        }
        return $total;
    }

    private function transactionTypeMinor(Collection $items, array $types, string $field): int
    {
        return $this->fieldMinor($items->whereIn('type', $types), $field);
    }

    private function amountRow(string $label, string $category, int $minor): array
    {
        return ['label' => $label, 'category' => $category, 'amount' => $this->money->fromMinor($minor), 'amount_minor' => $minor];
    }

    private function transactionTypeLabel(string $type): string
    {
        return match ($type) {
            'loan_payment' => 'Loan payments',
            'loan_early_settlement' => 'Early settlements',
            'loan_write_off_recovery' => 'Written-off loan recoveries',
            'loan_disbursement' => 'Loan disbursements',
            'loan_top_up' => 'Loan top-up cash',
            'expense' => 'Operating expenses',
            'payroll' => 'Payroll',
            'opening_balance' => 'Opening funds',
            'capital_injection' => 'Capital injections',
            'transfer_in' => 'Transfers in',
            'transfer_out' => 'Transfers out',
            default => str($type)->replace('_', ' ')->title()->toString(),
        };
    }
}
