<?php

namespace App\Services;

use App\Models\AppNotification;
use App\Models\Loan;
use App\Models\LoanPayment;
use App\Models\LoanPaymentAllocation;
use App\Models\LoanRepaymentSchedule;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class LoanPaymentService
{
    public function __construct(
        private readonly LoanCalculator $calculator,
        private readonly AccountingService $accounting,
        private readonly MemberNotificationAutomationService $notifications,
        private readonly TransactionApprovalSettings $approvalSettings,
    ) {
    }

    public function record(Loan $loan, float $amount, string $method, ?int $receivedBy = null, ?string $notes = null): LoanPayment
    {
        $payment = DB::transaction(function () use ($loan, $amount, $method, $receivedBy, $notes): LoanPayment {
            $this->calculator->applyLatePenalties($loan);
            $loan = Loan::whereKey($loan->id)->lockForUpdate()->firstOrFail();
            $pendingAmount = (float) LoanPayment::where('loan_id', $loan->id)
                ->where('status', LoanPayment::STATUS_PENDING)
                ->sum('amount');
            $availableBalance = max(0, (float) $loan->balance - $pendingAmount);

            if ($amount > $availableBalance) {
                throw new InvalidArgumentException('Payment exceeds outstanding balance after pending repayments.');
            }

            $payment = LoanPayment::create([
                'tenant_id' => $loan->tenant_id,
                'loan_id' => $loan->id,
                'loan_repayment_schedule_id' => null,
                'customer_id' => $loan->customer_id,
                'received_by' => $receivedBy,
                'amount' => $amount,
                'principal_component' => 0,
                'interest_component' => 0,
                'fee_component' => 0,
                'penalty_component' => 0,
                'balance_after' => $loan->balance,
                'reference' => NumberGenerator::reference('LRP'),
                'paid_at' => now(),
                'method' => $method,
                'status' => LoanPayment::STATUS_PENDING,
                'notes' => $notes,
            ]);

            ActivityLogger::log('loan_repayment_pending', 'Loan repayment '.$payment->reference.' submitted for approval on loan '.$loan->loan_number.'.', $payment);

            return $payment;
        });

        $payment = $payment->fresh(['loan.tenant', 'customer', 'receiver']);

        if ($payment->loan && ! $this->approvalSettings->requiresApproval($payment->loan->tenant, 'repayment')) {
            return $this->approve($payment, null);
        }

        return $payment;
    }

    public function approve(LoanPayment $payment, ?int $approvedBy = null): LoanPayment
    {
        $approvedPayment = DB::transaction(function () use ($payment, $approvedBy): LoanPayment {
            $lockedPayment = LoanPayment::whereKey($payment->id)->lockForUpdate()->firstOrFail();

            if ($lockedPayment->status !== LoanPayment::STATUS_PENDING) {
                throw new InvalidArgumentException('Only pending repayments can be approved.');
            }

            $loan = Loan::with('product')->whereKey($lockedPayment->loan_id)->lockForUpdate()->firstOrFail();
            $this->calculator->applyLatePenalties($loan);
            $loan = Loan::whereKey($loan->id)->lockForUpdate()->firstOrFail();
            $amount = (float) $lockedPayment->amount;

            if ($amount > (float) $loan->balance) {
                throw new InvalidArgumentException('Payment exceeds the current outstanding balance.');
            }

            $allocation = $this->allocateToSchedules($loan, $amount);
            $balanceAfter = max(0, round((float) $loan->balance - $amount, 2));

            $lockedPayment->update([
                'loan_repayment_schedule_id' => $allocation['first_schedule_id'],
                'principal_component' => $allocation['principal'],
                'interest_component' => $allocation['interest'],
                'fee_component' => 0,
                'penalty_component' => $allocation['penalty'],
                'balance_after' => $balanceAfter,
                'status' => LoanPayment::STATUS_POSTED,
                'approved_by' => $approvedBy,
                'approved_at' => now(),
                'rejected_by' => null,
                'rejected_at' => null,
                'rejected_reason' => null,
            ]);

            foreach ($allocation['allocations'] as $scheduleAllocation) {
                LoanPaymentAllocation::create($scheduleAllocation + [
                    'tenant_id' => $loan->tenant_id,
                    'loan_payment_id' => $lockedPayment->id,
                ]);
            }

            $loan->update([
                'balance' => $balanceAfter,
                'status' => $balanceAfter <= 0 ? 'completed' : 'active',
            ]);

            $postedPayment = $lockedPayment->fresh(['loan', 'customer', 'receiver', 'approver']);
            $this->accounting->recordLoanPayment($postedPayment);
            ActivityLogger::log('loan_repayment_approved', 'Loan repayment '.$postedPayment->reference.' approved and posted for loan '.$loan->loan_number.'.', $postedPayment);

            if ($balanceAfter <= 0) {
                AppNotification::create([
                    'tenant_id' => $loan->tenant_id,
                    'title' => 'Loan completed',
                    'body' => 'Loan '.$loan->loan_number.' has been fully repaid.',
                    'type' => 'loan',
                ]);
            }

            return $postedPayment;
        });

        if ($approvedPayment->customer) {
            $this->notifications->send($approvedPayment->customer, 'loan_repayment_received', [
                'amount' => $approvedPayment->amount,
                'balance_after' => $approvedPayment->balance_after,
                'loan_number' => $approvedPayment->loan?->loan_number,
                'reference' => $approvedPayment->reference,
            ]);
        }

        return $approvedPayment;
    }

    public function reject(LoanPayment $payment, string $reason, ?int $rejectedBy = null): LoanPayment
    {
        return DB::transaction(function () use ($payment, $reason, $rejectedBy): LoanPayment {
            $lockedPayment = LoanPayment::whereKey($payment->id)->lockForUpdate()->firstOrFail();

            if ($lockedPayment->status !== LoanPayment::STATUS_PENDING) {
                throw new InvalidArgumentException('Only pending repayments can be rejected.');
            }

            $lockedPayment->update([
                'status' => LoanPayment::STATUS_REJECTED,
                'rejected_by' => $rejectedBy,
                'rejected_at' => now(),
                'rejected_reason' => $reason,
            ]);

            ActivityLogger::log('loan_repayment_rejected', 'Loan repayment '.$lockedPayment->reference.' rejected.', $lockedPayment);

            return $lockedPayment->fresh(['loan', 'customer', 'receiver', 'rejecter']);
        });
    }

    /**
     * @return array{principal: float, interest: float, penalty: float, first_schedule_id: ?int, allocations: array<int, array<string, float|int>>}
     */
    private function allocateToSchedules(Loan $loan, float $amount): array
    {
        $remaining = $amount;
        $principal = 0.0;
        $interest = 0.0;
        $penalty = 0.0;
        $firstScheduleId = null;
        $allocations = [];

        $loan->schedules()
            ->whereIn('status', ['pending', 'partial', 'overdue'])
            ->orderBy('installment_no')
            ->lockForUpdate()
            ->get()
            ->each(function (LoanRepaymentSchedule $schedule) use (&$remaining, &$principal, &$interest, &$penalty, &$firstScheduleId, &$allocations): void {
                if ($remaining <= 0) {
                    return;
                }

                $outstanding = max(0, (float) $schedule->total_due - (float) $schedule->paid_amount);
                $applied = min($remaining, $outstanding);

                if ($applied <= 0) {
                    return;
                }

                $alreadyPaid = (float) $schedule->paid_amount;
                $penaltyOpen = max(0, (float) $schedule->penalty_due - $alreadyPaid);
                $paidAfterPenalty = max(0, $alreadyPaid - (float) $schedule->penalty_due);
                $interestOpen = max(0, (float) $schedule->interest_due - $paidAfterPenalty);
                $paidAfterInterest = max(0, $paidAfterPenalty - (float) $schedule->interest_due);
                $principalOpen = max(0, (float) $schedule->principal_due - $paidAfterInterest);

                $toPenalty = min($applied, $penaltyOpen);
                $toInterest = min($applied - $toPenalty, $interestOpen);
                $toPrincipal = min($applied - $toPenalty - $toInterest, $principalOpen);

                $penalty += $toPenalty;
                $interest += $toInterest;
                $principal += $toPrincipal;
                $remaining = round($remaining - $applied, 2);
                $firstScheduleId ??= $schedule->id;
                $allocations[] = [
                    'loan_repayment_schedule_id' => $schedule->id,
                    'principal_amount' => $toPrincipal,
                    'interest_amount' => $toInterest,
                    'penalty_amount' => $toPenalty,
                    'total_amount' => $applied,
                ];

                $newPaid = round((float) $schedule->paid_amount + $applied, 2);
                $schedule->update([
                    'paid_amount' => $newPaid,
                    'status' => $newPaid >= (float) $schedule->total_due ? 'paid' : 'partial',
                ]);
            });

        if ($remaining > 0.01) {
            throw new InvalidArgumentException('Payment could not be fully allocated to the repayment schedule.');
        }

        return [
            'principal' => round($principal, 2),
            'interest' => round($interest, 2),
            'penalty' => round($penalty, 2),
            'first_schedule_id' => $firstScheduleId,
            'allocations' => $allocations,
        ];
    }
}
