<?php

namespace App\Services;

use App\Models\Branch;
use App\Models\CashReconciliation;
use App\Models\DailyClosing;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\User;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class DailyClosingService
{
    public const CASH_ACCOUNT_TYPES = ['cash', 'petty_cash', 'safe'];

    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) {}

    /** @return array{date:CarbonImmutable,accounts:Collection,cash_accounts:Collection,summary:array,closing:?DailyClosing} */
    public function preview(Branch $branch, CarbonInterface|string $date): array
    {
        $day = $this->normalizeDate($date);
        $this->assertNotFuture($day);
        $branch->loadMissing('business');
        $accounts = $this->reconcilableAccounts($branch, $day);
        $rows = $accounts->map(function (FinancialAccount $account) use ($day): array {
            $snapshot = $this->accountSnapshot($account, $day);
            $reconciliation = CashReconciliation::query()
                ->with('reconciler')
                ->where('financial_account_id', $account->id)
                ->whereDate('reconciliation_date', $day->toDateString())
                ->where('status', 'active')
                ->latest('version')
                ->first();

            return [
                'account' => $account,
                'opening_cash' => $snapshot['opening_cash'],
                'cash_in' => $snapshot['cash_in'],
                'cash_out' => $snapshot['cash_out'],
                'expected_cash' => $snapshot['expected_cash'],
                'reconciliation' => $reconciliation,
                'stale' => $reconciliation
                    ? $this->money->toSignedMinor((string) $reconciliation->expected_cash) !== $this->money->toSignedMinor($snapshot['expected_cash'])
                    : false,
            ];
        });

        return [
            'date' => $day,
            'accounts' => $rows,
            // Legacy alias retained while existing callers migrate to the generic account terminology.
            'cash_accounts' => $rows,
            'summary' => $this->buildClosingSnapshot($branch, $day, $rows),
            'closing' => DailyClosing::query()->with(['preparer','managerConfirmer'])
                ->where('branch_id', $branch->id)
                ->whereDate('closing_date', $day->toDateString())
                ->first(),
        ];
    }

    public function createReconciliation(
        Branch $branch,
        FinancialAccount $account,
        CarbonInterface|string $date,
        string $actualBalance,
        ?string $varianceExplanation,
        User $user
    ): CashReconciliation {
        $day = $this->normalizeDate($date);
        $this->assertNotFuture($day);
        $actualMinor = $this->money->toMinor($actualBalance);

        return DB::transaction(function () use ($branch, $account, $day, $actualMinor, $varianceExplanation, $user): CashReconciliation {
            $lockedBranch = Branch::query()->with('business')->lockForUpdate()->findOrFail($branch->id);
            $lockedAccount = FinancialAccount::query()->lockForUpdate()->findOrFail($account->id);
            $this->assertReconcilableAccount($lockedBranch, $lockedAccount, $day);
            $this->assertDayNotConfirmed($lockedBranch, $day);

            $snapshot = $this->accountSnapshot($lockedAccount, $day);
            $expectedMinor = $this->money->toSignedMinor($snapshot['expected_cash']);
            $varianceMinor = $actualMinor - $expectedMinor;
            $explanation = trim((string) $varianceExplanation);
            if ($varianceMinor !== 0 && $explanation === '') {
                throw ValidationException::withMessages([
                    'variance_explanation' => 'Explain the account variance before saving this reconciliation.',
                ]);
            }

            $current = CashReconciliation::query()
                ->where('financial_account_id', $lockedAccount->id)
                ->whereDate('reconciliation_date', $day->toDateString())
                ->where('status', 'active')
                ->lockForUpdate()
                ->first();
            $version = ((int) CashReconciliation::query()
                ->where('financial_account_id', $lockedAccount->id)
                ->whereDate('reconciliation_date', $day->toDateString())
                ->max('version')) + 1;

            $reconciliation = CashReconciliation::create([
                'reconciliation_number' => $this->nextNumber('cash_reconciliation', 'REC', $lockedBranch, $day),
                'business_id' => $lockedBranch->business_id,
                'branch_id' => $lockedBranch->id,
                'financial_account_id' => $lockedAccount->id,
                'reconciliation_date' => $day->toDateString(),
                'version' => $version,
                'status' => 'active',
                // These legacy column names store generic account reconciliation values for backwards compatibility.
                'opening_cash' => $snapshot['opening_cash'],
                'cash_in' => $snapshot['cash_in'],
                'cash_out' => $snapshot['cash_out'],
                'expected_cash' => $snapshot['expected_cash'],
                'physical_cash' => $this->money->fromMinor($actualMinor),
                'variance' => $this->money->fromSignedMinor($varianceMinor),
                'variance_explanation' => $varianceMinor === 0 ? null : $explanation,
                'reconciled_by' => $user->id,
                'reconciled_at' => now(),
            ]);

            if ($current) {
                $current->update([
                    'status' => 'superseded',
                    'superseded_by_id' => $reconciliation->id,
                    'superseded_at' => now(),
                ]);
            }

            return $reconciliation->fresh(['branch','financialAccount','reconciler','supersededBy']);
        });
    }

    public function prepareOrRefresh(Branch $branch, CarbonInterface|string $date, User $user): DailyClosing
    {
        $day = $this->normalizeDate($date);
        $this->assertNotFuture($day);

        return DB::transaction(function () use ($branch, $day, $user): DailyClosing {
            $lockedBranch = Branch::query()->with('business')->lockForUpdate()->findOrFail($branch->id);
            $existing = DailyClosing::query()
                ->where('branch_id', $lockedBranch->id)
                ->whereDate('closing_date', $day->toDateString())
                ->lockForUpdate()
                ->first();
            if ($existing?->status === 'confirmed') {
                throw ValidationException::withMessages(['closing' => 'This branch day is already manager-confirmed and permanently closed.']);
            }

            $rows = $this->reconciledAccountRows($lockedBranch, $day, true);
            $snapshot = $this->buildClosingSnapshot($lockedBranch, $day, $rows);
            $attributes = $this->closingAttributes($snapshot) + [
                'business_id' => $lockedBranch->business_id,
                'branch_id' => $lockedBranch->id,
                'closing_date' => $day->toDateString(),
                'status' => 'prepared',
                'prepared_by' => $user->id,
                'prepared_at' => now(),
                'manager_confirmed_by' => null,
                'manager_confirmed_at' => null,
                'manager_comments' => null,
            ];

            if ($existing) {
                $existing->update($attributes);
                return $existing->fresh(['branch','preparer','managerConfirmer']);
            }

            $attributes['closing_number'] = $this->nextNumber('daily_closing', 'CLS', $lockedBranch, $day);
            return DailyClosing::create($attributes)->fresh(['branch','preparer','managerConfirmer']);
        });
    }

    public function confirm(DailyClosing $closing, User $manager, ?string $comments = null): DailyClosing
    {
        return DB::transaction(function () use ($closing, $manager, $comments): DailyClosing {
            $locked = DailyClosing::query()->with('branch.business')->lockForUpdate()->findOrFail($closing->id);
            if ($locked->status !== 'prepared') {
                throw ValidationException::withMessages(['closing' => 'Only a prepared daily closing can be manager-confirmed.']);
            }
            if ($locked->branch->business->maker_checker_enabled && (int) $locked->prepared_by === (int) $manager->id) {
                throw ValidationException::withMessages(['closing' => 'Maker-checker is enabled. The employee who prepared the closing cannot confirm it.']);
            }

            $day = CarbonImmutable::parse($locked->closing_date)->startOfDay();
            $rows = $this->reconciledAccountRows($locked->branch, $day, true);
            $current = $this->buildClosingSnapshot($locked->branch, $day, $rows);
            if (! hash_equals((string) $locked->snapshot_fingerprint, $current['snapshot_fingerprint'])) {
                throw ValidationException::withMessages([
                    'closing' => 'The financial ledger or an account reconciliation changed after this closing was prepared. Refresh the closing and review it again before confirmation.',
                ]);
            }

            $managerComments = trim((string) $comments);
            if ($this->money->toSignedMinor((string) $locked->variance) !== 0 && $managerComments === '') {
                throw ValidationException::withMessages([
                    'manager_comments' => 'Manager comments are required when the branch closes with an account variance.',
                ]);
            }

            $locked->update([
                'status' => 'confirmed',
                'manager_confirmed_by' => $manager->id,
                'manager_confirmed_at' => now(),
                'manager_comments' => $managerComments === '' ? null : $managerComments,
            ]);

            return $locked->fresh(['branch','preparer','managerConfirmer']);
        });
    }

    /** @return Collection<int, array{account:FinancialAccount,opening_cash:string,cash_in:string,cash_out:string,expected_cash:string,reconciliation:CashReconciliation,stale:bool}> */
    private function reconciledAccountRows(Branch $branch, CarbonImmutable $day, bool $requireCurrent): Collection
    {
        return $this->reconcilableAccounts($branch, $day)->map(function (FinancialAccount $account) use ($day, $requireCurrent): array {
            $snapshot = $this->accountSnapshot($account, $day);
            $reconciliation = CashReconciliation::query()
                ->where('financial_account_id', $account->id)
                ->whereDate('reconciliation_date', $day->toDateString())
                ->where('status', 'active')
                ->lockForUpdate()
                ->latest('version')
                ->first();

            if (! $reconciliation) {
                throw ValidationException::withMessages([
                    'reconciliation' => 'Reconcile '.$account->name.' before preparing the branch daily closing.',
                ]);
            }

            $stale = $this->money->toSignedMinor((string) $reconciliation->expected_cash) !== $this->money->toSignedMinor($snapshot['expected_cash']);
            if ($requireCurrent && $stale) {
                throw ValidationException::withMessages([
                    'reconciliation' => $account->name.' has new financial movements after its last reconciliation. Reconcile the account again before closing.',
                ]);
            }

            return [
                'account' => $account,
                'opening_cash' => $snapshot['opening_cash'],
                'cash_in' => $snapshot['cash_in'],
                'cash_out' => $snapshot['cash_out'],
                'expected_cash' => $snapshot['expected_cash'],
                'reconciliation' => $reconciliation,
                'stale' => $stale,
            ];
        });
    }

    /** @return Collection<int, FinancialAccount> */
    private function reconcilableAccounts(Branch $branch, CarbonImmutable $day): Collection
    {
        return FinancialAccount::query()
            ->where('branch_id', $branch->id)
            ->where('created_at', '<=', $day->endOfDay())
            ->orderBy('account_type')
            ->orderBy('name')
            ->get();
    }

    /** @return array{opening_cash:string,cash_in:string,cash_out:string,expected_cash:string} */
    private function accountSnapshot(FinancialAccount $account, CarbonImmutable $day): array
    {
        $openingMinor = 0;
        foreach (FinancialAccountTransaction::query()
            ->where('account_id', $account->id)
            ->where('transaction_date', '<', $day->startOfDay())
            ->orderBy('id')
            ->cursor() as $transaction) {
            $openingMinor += $this->money->toMinor((string) $transaction->money_in);
            $openingMinor -= $this->money->toMinor((string) $transaction->money_out);
        }

        $inMinor = 0;
        $outMinor = 0;
        foreach (FinancialAccountTransaction::query()
            ->where('account_id', $account->id)
            ->whereBetween('transaction_date', [$day->startOfDay(), $day->endOfDay()])
            ->orderBy('id')
            ->cursor() as $transaction) {
            $inMinor += $this->money->toMinor((string) $transaction->money_in);
            $outMinor += $this->money->toMinor((string) $transaction->money_out);
        }

        return [
            'opening_cash' => $this->money->fromSignedMinor($openingMinor),
            'cash_in' => $this->money->fromMinor($inMinor),
            'cash_out' => $this->money->fromMinor($outMinor),
            'expected_cash' => $this->money->fromSignedMinor($openingMinor + $inMinor - $outMinor),
        ];
    }

    /** @param Collection<int,array> $accountRows */
    private function buildClosingSnapshot(Branch $branch, CarbonImmutable $day, Collection $accountRows): array
    {
        $openingMinor = 0;
        $openingInMinor = 0;
        $openingOutMinor = 0;
        $openingCount = 0;
        $openingMaxId = 0;
        foreach (FinancialAccountTransaction::query()
            ->where('branch_id', $branch->id)
            ->where('transaction_date', '<', $day->startOfDay())
            ->orderBy('id')
            ->cursor() as $transaction) {
            $in = $this->money->toMinor((string) $transaction->money_in);
            $out = $this->money->toMinor((string) $transaction->money_out);
            $openingMinor += $in - $out;
            $openingInMinor += $in;
            $openingOutMinor += $out;
            $openingCount++;
            $openingMaxId = max($openingMaxId, (int) $transaction->id);
        }

        $dayTransactions = FinancialAccountTransaction::query()
            ->with('account:id,account_type')
            ->where('branch_id', $branch->id)
            ->whereBetween('transaction_date', [$day->startOfDay(), $day->endOfDay()])
            ->orderBy('id')
            ->get();

        $dayIn = $dayOut = $collections = $cashCollections = $mobileCollections = $bankCollections = $otherCollections = 0;
        $disbursements = $expenses = $transfers = $capitalInflows = $otherInflows = $otherOutflows = 0;

        foreach ($dayTransactions as $transaction) {
            $in = $this->money->toMinor((string) $transaction->money_in);
            $out = $this->money->toMinor((string) $transaction->money_out);
            $dayIn += $in;
            $dayOut += $out;
            $type = (string) $transaction->type;

            if (in_array($type, self::COLLECTION_TYPES, true)) {
                $collections += $in;
                $accountType = $transaction->account?->account_type;
                if (in_array($accountType, self::CASH_ACCOUNT_TYPES, true)) $cashCollections += $in;
                elseif ($accountType === 'mobile_money') $mobileCollections += $in;
                elseif ($accountType === 'bank') $bankCollections += $in;
                else $otherCollections += $in;
                continue;
            }
            if (in_array($type, self::DISBURSEMENT_TYPES, true)) {
                $disbursements += $out;
                continue;
            }
            if (in_array($type, self::EXPENSE_TYPES, true)) {
                $expenses += $out;
                continue;
            }
            if ($type === 'transfer_in' || $type === 'transfer_out') {
                $transfers += $in - $out;
                continue;
            }
            if (in_array($type, self::CAPITAL_TYPES, true)) {
                $capitalInflows += $in;
                continue;
            }
            $otherInflows += $in;
            $otherOutflows += $out;
        }

        $closingMinor = $openingMinor + $dayIn - $dayOut;
        $expectedFundsMinor = 0;
        $actualFundsMinor = 0;
        $reconciliations = [];
        foreach ($accountRows as $row) {
            $expectedFundsMinor += $this->money->toSignedMinor((string) $row['expected_cash']);
            if ($row['reconciliation'] ?? null) {
                /** @var CashReconciliation $reconciliation */
                $reconciliation = $row['reconciliation'];
                $actualFundsMinor += $this->money->toMinor((string) $reconciliation->physical_cash);
                $reconciliations[] = [
                    'id' => $reconciliation->id,
                    'account_id' => $row['account']->id,
                    'account_type' => $row['account']->account_type,
                    'version' => $reconciliation->version,
                    'expected_cash' => (string) $reconciliation->expected_cash,
                    'physical_cash' => (string) $reconciliation->physical_cash,
                    'variance' => (string) $reconciliation->variance,
                ];
            }
        }
        $varianceMinor = $actualFundsMinor - $expectedFundsMinor;

        $fingerprintPayload = [
            'branch_id' => $branch->id,
            'date' => $day->toDateString(),
            'opening' => [$openingCount, $openingMaxId, $openingInMinor, $openingOutMinor],
            'day_transactions' => $dayTransactions->map(fn (FinancialAccountTransaction $transaction): array => [
                (int) $transaction->id,
                (int) $transaction->account_id,
                (string) $transaction->type,
                (string) $transaction->money_in,
                (string) $transaction->money_out,
            ])->all(),
            'reconciliations' => $reconciliations,
        ];

        return [
            'opening_funds' => $this->money->fromSignedMinor($openingMinor),
            'collections' => $this->money->fromMinor($collections),
            'cash_collections' => $this->money->fromMinor($cashCollections),
            'mobile_money_collections' => $this->money->fromMinor($mobileCollections),
            'bank_collections' => $this->money->fromMinor($bankCollections),
            'other_collections' => $this->money->fromMinor($otherCollections),
            'disbursements' => $this->money->fromMinor($disbursements),
            'expenses' => $this->money->fromMinor($expenses),
            'transfers' => $this->money->fromSignedMinor($transfers),
            'capital_inflows' => $this->money->fromMinor($capitalInflows),
            'other_inflows' => $this->money->fromMinor($otherInflows),
            'other_outflows' => $this->money->fromMinor($otherOutflows),
            'closing_funds' => $this->money->fromSignedMinor($closingMinor),
            // Legacy DailyClosing column names now represent all reconciled branch funds, not cash-only values.
            'expected_cash' => $this->money->fromSignedMinor($expectedFundsMinor),
            'actual_cash' => $this->money->fromMinor($actualFundsMinor),
            'variance' => $this->money->fromSignedMinor($varianceMinor),
            'snapshot_fingerprint' => hash('sha256', json_encode($fingerprintPayload, JSON_THROW_ON_ERROR)),
            'snapshot' => [
                'transaction_count' => $dayTransactions->count(),
                'transaction_ids' => $dayTransactions->pluck('id')->all(),
                'opening_transaction_count' => $openingCount,
                'reconciled_account_count' => count($reconciliations),
                'reconciliations' => $reconciliations,
                'equation' => [
                    'opening_funds' => $this->money->fromSignedMinor($openingMinor),
                    'money_in' => $this->money->fromMinor($dayIn),
                    'money_out' => $this->money->fromMinor($dayOut),
                    'closing_funds' => $this->money->fromSignedMinor($closingMinor),
                ],
            ],
        ];
    }

    private function closingAttributes(array $snapshot): array
    {
        return collect($snapshot)->only([
            'opening_funds', 'collections', 'cash_collections', 'mobile_money_collections', 'bank_collections',
            'other_collections', 'disbursements', 'expenses', 'transfers', 'capital_inflows', 'other_inflows',
            'other_outflows', 'closing_funds', 'expected_cash', 'actual_cash', 'variance', 'snapshot_fingerprint', 'snapshot',
        ])->all();
    }

    private function assertReconcilableAccount(Branch $branch, FinancialAccount $account, CarbonImmutable $day): void
    {
        if ($account->branch_id !== $branch->id) {
            throw ValidationException::withMessages(['financial_account_id' => 'The financial account must belong to the selected branch.']);
        }
        if ($account->created_at && $account->created_at->gt($day->endOfDay())) {
            throw ValidationException::withMessages(['financial_account_id' => 'This financial account did not exist on the selected closing date.']);
        }
    }

    private function assertDayNotConfirmed(Branch $branch, CarbonImmutable $day): void
    {
        if (DailyClosing::query()->where('branch_id', $branch->id)->where('status', 'confirmed')->whereDate('closing_date', $day->toDateString())->exists()) {
            throw ValidationException::withMessages(['reconciliation' => 'This branch day is already manager-confirmed and cannot be reconciled again.']);
        }
    }

    private function assertNotFuture(CarbonImmutable $day): void
    {
        if ($day->gt(CarbonImmutable::today())) {
            throw ValidationException::withMessages(['date' => 'Account reconciliation and daily closing cannot be created for a future date.']);
        }
    }

    private function normalizeDate(CarbonInterface|string $date): CarbonImmutable
    {
        return $date instanceof CarbonInterface
            ? CarbonImmutable::instance($date)->startOfDay()
            : CarbonImmutable::parse((string) $date)->startOfDay();
    }

    private function nextNumber(string $type, string $prefix, Branch $branch, CarbonInterface $date): string
    {
        $year = (int) $date->format('Y');
        DB::table('number_sequences')->insertOrIgnore([
            'type' => $type,
            'branch_id' => $branch->id,
            'year' => $year,
            'current_value' => 0,
            'created_at' => now(),
            'updated_at' => now(),
        ]);
        $row = DB::table('number_sequences')
            ->where('type', $type)
            ->where('branch_id', $branch->id)
            ->where('year', $year)
            ->lockForUpdate()
            ->first();
        $next = ((int) $row->current_value) + 1;
        DB::table('number_sequences')->where('id', $row->id)->update(['current_value' => $next, 'updated_at' => now()]);
        $tag = strtoupper(preg_replace('/[^A-Z0-9]/', '', str_replace('BR-', '', $branch->code ?? '')) ?: (string) $branch->id);
        return $prefix.'-'.$tag.'-'.$year.'-'.str_pad((string) $next, 6, '0', STR_PAD_LEFT);
    }
}
