<?php

namespace App\Services;

use App\Models\CustomerStatementEntry;
use App\Models\Loan;
use App\Models\LoanInstallment;
use App\Models\LoanPenaltyCharge;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;

class LoanPenaltyService
{
    public function __construct(
        private MoneyMath $money,
        private CustomerStatementBalanceService $statements,
    ) {}

    public function assess(Loan $loan, CarbonInterface $asOf): int
    {
        $type = (string) ($loan->late_payment_penalty_type ?: 'none');
        $value = (string) ($loan->late_payment_penalty_value ?: '0');
        if ($type === 'none' || $this->money->toMinor($value) === 0 && $type === 'fixed') {
            return 0;
        }

        $target = CarbonImmutable::instance($asOf)->startOfDay();
        $totalAdded = 0;
        $installments = $loan->installments()->orderBy('installment_number')->lockForUpdate()->get();

        foreach ($installments as $installment) {
            $due = CarbonImmutable::parse($installment->due_date)->startOfDay();
            if (! $due->lessThan($target)) {
                continue;
            }

            $added = 0;
            foreach ($this->effectiveDates($type, $due, $target) as [$effectiveDate, $periodKey, $daysOverdue]) {
                if (LoanPenaltyCharge::query()->where('loan_installment_id', $installment->id)->where('period_key', $periodKey)->exists()) {
                    continue;
                }

                $baseMinor = $this->scheduledBaseMinorAt($installment, $effectiveDate);
                if ($baseMinor <= 0) {
                    continue;
                }

                $amountMinor = $type === 'fixed'
                    ? $this->money->toMinor($value)
                    : $this->money->percentageOf($baseMinor, $value);

                if ($amountMinor <= 0) {
                    continue;
                }

                $charge = LoanPenaltyCharge::create([
                    'loan_id' => $loan->id,
                    'loan_installment_id' => $installment->id,
                    'branch_id' => $loan->branch_id,
                    'effective_date' => $effectiveDate,
                    'period_key' => $periodKey,
                    'penalty_type' => $type,
                    'penalty_value' => $value,
                    'base_amount' => $this->money->fromMinor($baseMinor),
                    'amount' => $this->money->fromMinor($amountMinor),
                    'days_overdue' => $daysOverdue,
                    'source' => 'automated',
                ]);

                CustomerStatementEntry::create([
                    'customer_id' => $loan->customer_id,
                    'loan_id' => $loan->id,
                    'branch_id' => $loan->branch_id,
                    'entry_date' => $effectiveDate->startOfDay(),
                    'reference' => 'PEN-'.str_pad((string) $charge->id, 8, '0', STR_PAD_LEFT),
                    'type' => 'loan_penalty',
                    'description' => 'Late-payment penalty for installment #'.$installment->installment_number,
                    'debit' => $this->money->fromMinor($amountMinor),
                    'credit' => '0.00',
                    'running_balance' => '0.00',
                    'related_type' => LoanPenaltyCharge::class,
                    'related_id' => $charge->id,
                ]);

                $added += $amountMinor;
            }

            if ($added > 0) {
                $newPenaltyDue = $this->money->toMinor((string) $installment->penalty_due) + $added;
                $newRemaining = $this->money->toMinor((string) $installment->remaining) + $added;
                $installment->update([
                    'penalty_due' => $this->money->fromMinor($newPenaltyDue),
                    'remaining' => $this->money->fromMinor($newRemaining),
                    'status' => 'overdue',
                ]);
                $totalAdded += $added;
            }
        }

        if ($totalAdded > 0) {
            $loan->update([
                'penalties_outstanding' => $this->money->fromMinor($this->money->toMinor((string) ($loan->penalties_outstanding ?? '0')) + $totalAdded),
                'total_outstanding' => $this->money->fromMinor($this->money->toMinor((string) ($loan->total_outstanding ?? '0')) + $totalAdded),
            ]);
            $this->statements->recalculate($loan);
        }

        return $totalAdded;
    }

    /** @return list<array{0:CarbonImmutable,1:string,2:int}> */
    private function effectiveDates(string $type, CarbonImmutable $due, CarbonImmutable $target): array
    {
        $first = $due->addDay();
        if ($first->greaterThan($target)) {
            return [];
        }

        if (in_array($type, ['fixed', 'percentage'], true)) {
            return [[$first, 'once', 1]];
        }

        $dates = [];
        $step = $type === 'weekly_percentage' ? 7 : 1;
        $cursor = $first;
        $period = 0;
        while ($cursor->lessThanOrEqualTo($target)) {
            $period++;
            $daysOverdue = (int) $due->diffInDays($cursor);
            $key = $type === 'weekly_percentage' ? 'week-'.$period : $cursor->toDateString();
            $dates[] = [$cursor, $key, $daysOverdue];
            $cursor = $cursor->addDays($step);
            if ($period > 5000) {
                break;
            }
        }

        return $dates;
    }

    private function scheduledBaseMinorAt(LoanInstallment $installment, CarbonImmutable $effectiveDate): int
    {
        $paidPrincipal = 0;
        $paidInterest = 0;
        $paidFees = 0;
        $allocations = $installment->paymentAllocations()->with('payment')->get();

        foreach ($allocations as $allocation) {
            if (! $allocation->payment || ! CarbonImmutable::parse($allocation->payment->payment_date)->startOfDay()->lessThan($effectiveDate)) {
                continue;
            }
            $paidPrincipal += $this->money->toMinor((string) $allocation->principal);
            $paidInterest += $this->money->toMinor((string) $allocation->interest);
            $paidFees += $this->money->toMinor((string) $allocation->fees);
        }

        $principal = max(0, $this->money->toMinor((string) $installment->principal) - $paidPrincipal);
        $interest = max(0, $this->money->toMinor((string) $installment->interest) - $paidInterest);
        $fees = max(0, $this->money->toMinor((string) $installment->fees) - $paidFees);

        return $principal + $interest + $fees;
    }
}
