<?php

namespace App\Services;

use App\Models\Loan;
use App\Models\LoanProduct;
use App\Models\LoanRepaymentSchedule;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

class LoanCalculator
{
    public const INTEREST_METHODS = [
        'flat' => 'Flat interest',
        'reducing_balance' => 'Reducing balance',
        'interest_only' => 'Interest only',
        'upfront' => 'Upfront interest',
        'custom' => 'Custom interest',
        'none' => 'No interest',
    ];

    public const REPAYMENT_FREQUENCIES = [
        'daily' => 'Daily',
        'weekly' => 'Weekly',
        'biweekly' => 'Every 2 weeks',
        'monthly' => 'Monthly',
        'bimonthly' => 'Every 2 months',
        'quarterly' => 'Quarterly',
        'semiannual' => 'Every 6 months',
        'yearly' => 'Yearly',
        'single' => 'Single payment at maturity',
        'custom' => 'Custom per loan',
    ];

    public const CUSTOM_REPAYMENT_FREQUENCIES = [
        'daily' => 'Daily',
        'weekly' => 'Weekly',
        'biweekly' => 'Every 2 weeks',
        'monthly' => 'Monthly',
        'bimonthly' => 'Every 2 months',
        'quarterly' => 'Quarterly',
        'semiannual' => 'Every 6 months',
        'yearly' => 'Yearly',
        'single' => 'Single payment at maturity',
    ];

    public const TERM_UNITS = [
        'days' => 'Days',
        'weeks' => 'Weeks',
        'months' => 'Months',
        'years' => 'Years',
    ];

    public const AMOUNT_TYPES = [
        'percentage' => 'Percentage',
        'fixed' => 'Fixed amount',
    ];

    public const REQUIREMENT_OPTIONS = [
        'required' => 'Required',
        'optional' => 'Optional',
        'hidden' => 'Hidden',
    ];

    public function quote(float $principal, LoanProduct $product, ?int $termDays = null, ?string $repaymentFrequency = null, ?float $interestRate = null, ?int $termValue = null): array
    {
        $actualTermDays = max(1, $termDays ?: (int) $product->term_days);
        $frequency = $this->scheduleFrequency($repaymentFrequency ?: $product->repayment_frequency);
        $installments = $this->installmentsForPeriods((int) ($termValue ?: $product->term_value ?: 1), $frequency, $actualTermDays);
        $interest = $this->interestAmount(
            $principal,
            $interestRate ?? (float) $product->interest_rate,
            $installments,
            (string) $product->interest_method
        );
        $processingFee = $this->processingFee($principal, $product);
        $total = round($principal + $interest, 2);

        return [
            'interest_amount' => $interest,
            'processing_fee' => $processingFee,
            'total_payable' => $total,
            'installments' => $installments,
        ];
    }

    public function createSchedule(Loan $loan): void
    {
        $scheduleVersion = LoanRepaymentSchedule::withoutGlobalScope('current_schedule')
            ->where('tenant_id', $loan->tenant_id)
            ->where('loan_id', $loan->id)
            ->where('is_current', false)
            ->exists()
                ? ((int) LoanRepaymentSchedule::withoutGlobalScope('current_schedule')
                    ->where('tenant_id', $loan->tenant_id)
                    ->where('loan_id', $loan->id)
                    ->max('schedule_version')) + 1
                : 1;

        $loan->schedules()->delete();

        $frequency = $this->scheduleFrequency($loan->repayment_frequency);
        $installments = $this->expectedInstallments($loan);
        $method = $this->normalInterestMethod((string) ($loan->interest_method ?: $loan->product?->interest_method));
        $start = Carbon::parse($loan->disbursed_at ?? $loan->application_date ?? now())->startOfDay();
        $firstDue = $loan->first_repayment_date
            ? Carbon::parse($loan->first_repayment_date)->startOfDay()
            : null;

        $principalAllocated = 0.0;
        $interestAllocated = 0.0;
        $outstandingPrincipal = (float) $loan->principal;
        $lastDueDate = null;

        for ($i = 1; $i <= $installments; $i++) {
            $principalDue = $this->principalDue((float) $loan->principal, $installments, $i, $method, $principalAllocated);
            $interestDue = $this->interestDue($loan, $installments, $i, $method, $interestAllocated, $outstandingPrincipal);

            $dueDate = $firstDue
                ? $this->addFrequency($firstDue, $frequency, $i - 1, (int) $loan->term_days)
                : $this->nextDueDate($start, $frequency, $i, (int) $loan->term_days);

            LoanRepaymentSchedule::create([
                'tenant_id' => $loan->tenant_id,
                'loan_id' => $loan->id,
                'installment_no' => $i,
                'schedule_version' => $scheduleVersion,
                'is_current' => true,
                'due_date' => $dueDate,
                'principal_due' => $principalDue,
                'interest_due' => $interestDue,
                'fee_due' => 0,
                'penalty_due' => 0,
                'total_due' => round($principalDue + $interestDue, 2),
                'paid_amount' => 0,
                'status' => 'pending',
            ]);

            $principalAllocated += $principalDue;
            $interestAllocated += $interestDue;
            $outstandingPrincipal = max(0, $outstandingPrincipal - $principalDue);
            $lastDueDate = $dueDate->toDateString();
        }

        if ($lastDueDate) {
            $loan->forceFill([
                'due_date' => $lastDueDate,
                'balance' => $this->outstandingBalance($loan->fresh()),
            ])->saveQuietly();
        }
    }

    public function rebuildSchedulePreservingPayments(Loan $loan): void
    {
        $paidAmount = (float) $loan->payments()->posted()->sum('amount');

        $this->createSchedule($loan->fresh());

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

        $remaining = $paidAmount;

        $loan->fresh()
            ->schedules()
            ->orderBy('installment_no')
            ->get()
            ->each(function (LoanRepaymentSchedule $schedule) use (&$remaining): void {
                if ($remaining <= 0) {
                    return;
                }

                $applied = min($remaining, (float) $schedule->total_due);
                $remaining -= $applied;

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

        $this->syncLoanBalance($loan->fresh());
    }

    public function applyLatePenalties(Loan $loan): void
    {
        $value = (float) ($loan->product?->penalty_rate ?? 0);

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

        $type = $loan->product?->penalty_type === 'fixed' ? 'fixed' : 'percentage';

        $loan->schedules()
            ->whereIn('status', ['pending', 'partial', 'overdue'])
            ->whereDate('due_date', '<', today())
            ->get()
            ->each(function (LoanRepaymentSchedule $schedule) use ($type, $value): void {
                $days = Carbon::parse($schedule->due_date)->diffInDays(today());
                $base = (float) $schedule->principal_due + (float) $schedule->interest_due;
                $penalty = $type === 'fixed'
                    ? round($value * $days, 2)
                    : round($base * ($value / 100) * $days, 2);

                $schedule->update([
                    'penalty_due' => $penalty,
                    'total_due' => round($base + $penalty, 2),
                    'status' => $schedule->paid_amount > 0 ? 'partial' : 'overdue',
                ]);
            });

        $this->syncLoanBalance($loan->fresh());
    }

    public function outstandingBalance(Loan $loan): float
    {
        $loan->loadMissing('schedules');

        $balance = $loan->schedules
            ->sum(fn (LoanRepaymentSchedule $schedule) => max(0, (float) $schedule->total_due - (float) $schedule->paid_amount));

        return round((float) $balance, 2);
    }

    public function nextOutstandingScheduleAmount(Loan $loan): float
    {
        $schedule = $loan->schedules()
            ->whereIn('status', ['pending', 'partial', 'overdue'])
            ->orderBy('installment_no')
            ->first();

        if (! $schedule) {
            return max(0, (float) $loan->balance);
        }

        return round(max(0, (float) $schedule->total_due - (float) $schedule->paid_amount), 2);
    }

    public function syncLoanBalance(Loan $loan): void
    {
        if (! in_array($loan->status, ['active', 'disbursed', 'completed'], true) && (float) $loan->amount_disbursed <= 0) {
            return;
        }

        $balance = $this->outstandingBalance($loan);

        $loan->forceFill([
            'balance' => $balance,
            'status' => $balance <= 0 && in_array($loan->status, ['active', 'disbursed'], true) ? 'completed' : $loan->status,
        ])->saveQuietly();
    }

    public function defaultFirstRepaymentDate(string $frequency, ?Carbon $base = null, int $termDays = 30): Carbon
    {
        return $this->nextDueDate(($base ?: today())->copy()->startOfDay(), $this->scheduleFrequency($frequency), 1, $termDays);
    }

    public function rescheduleRemaining(Loan $loan, int $installments, ?string $frequency = null, ?string $firstDueDate = null, array $adjustments = [], ?string $note = null, ?int $userId = null): void
    {
        $this->applyLatePenalties($loan);

        DB::transaction(function () use ($loan, $installments, $frequency, $firstDueDate, $adjustments, $note, $userId): void {
            $loan = $loan->fresh();
            $installments = max(1, $installments);
            $frequency = $this->scheduleFrequency($frequency ?: $loan->repayment_frequency);
            $firstDue = $firstDueDate
                ? Carbon::parse($firstDueDate)->startOfDay()
                : $this->defaultFirstRepaymentDate($frequency, today(), (int) $loan->term_days);

            $currentSchedules = $loan->schedules()
                ->orderBy('installment_no')
                ->lockForUpdate()
                ->get();

            $openSchedules = $currentSchedules
                ->whereIn('status', ['pending', 'partial', 'overdue'])
                ->values();

            if ($openSchedules->isEmpty()) {
                return;
            }

            $componentTotals = [
                'principal_due' => 0.0,
                'interest_due' => 0.0,
                'penalty_due' => 0.0,
            ];

            foreach ($openSchedules as $schedule) {
                foreach ($this->outstandingComponents($schedule) as $key => $amount) {
                    $componentTotals[$key] += $amount;
                }
            }

            foreach ([
                'principal_due' => 'principal_adjustment',
                'interest_due' => 'interest_adjustment',
                'penalty_due' => 'penalty_adjustment',
            ] as $component => $inputKey) {
                $componentTotals[$component] = max(0, round($componentTotals[$component] + (float) ($adjustments[$inputKey] ?? 0), 2));
            }

            $newVersion = ((int) LoanRepaymentSchedule::withoutGlobalScope('current_schedule')
                ->where('tenant_id', $loan->tenant_id)
                ->where('loan_id', $loan->id)
                ->max('schedule_version')) + 1;

            $loan->schedules()->update([
                'is_current' => false,
                'archived_at' => now(),
                'rescheduled_by' => $userId,
                'reschedule_note' => $note,
            ]);

            $allocated = [
                'principal_due' => 0.0,
                'interest_due' => 0.0,
                'penalty_due' => 0.0,
            ];

            $lastDueDate = null;

            for ($i = 1; $i <= $installments; $i++) {
                $row = [];

                foreach ($componentTotals as $key => $total) {
                    $row[$key] = $i === $installments
                        ? round($total - $allocated[$key], 2)
                        : round($total / $installments, 2);
                    $allocated[$key] += $row[$key];
                }

                $dueDate = $this->addFrequency($firstDue, $frequency, $i - 1, (int) $loan->term_days);
                $lastDueDate = $dueDate->toDateString();

                LoanRepaymentSchedule::create([
                    'tenant_id' => $loan->tenant_id,
                    'loan_id' => $loan->id,
                    'installment_no' => $i,
                    'schedule_version' => $newVersion,
                    'is_current' => true,
                    'due_date' => $dueDate,
                    'principal_due' => $row['principal_due'],
                    'interest_due' => $row['interest_due'],
                    'fee_due' => 0,
                    'penalty_due' => $row['penalty_due'],
                    'total_due' => round(array_sum($row), 2),
                    'paid_amount' => 0,
                    'status' => 'pending',
                ]);
            }

            $currentVersionSchedules = LoanRepaymentSchedule::withoutGlobalScope('current_schedule')
                ->where('loan_id', $loan->id)
                ->where('schedule_version', $newVersion)
                ->get();
            $termValue = $installments;

            $loan->forceFill([
                'repayment_frequency' => $frequency,
                'term_value' => $termValue,
                'term_unit' => $this->termUnitForFrequency($frequency),
                'term_days' => $this->termDaysForPeriods($termValue, $frequency),
                'first_repayment_date' => $firstDue->toDateString(),
                'due_date' => $lastDueDate,
                'interest_amount' => round((float) $currentVersionSchedules->sum('interest_due'), 2),
                'total_payable' => round((float) $currentVersionSchedules->sum('principal_due') + (float) $currentVersionSchedules->sum('interest_due'), 2),
            ])->saveQuietly();

            $this->syncLoanBalance($loan->fresh());
        });
    }

    public function installmentCount(int $termDays, string $frequency): int
    {
        $termDays = max(1, $termDays);

        return max(1, match ($this->scheduleFrequency($frequency)) {
            'daily' => $termDays,
            'weekly' => (int) ceil($termDays / 7),
            'biweekly' => (int) ceil($termDays / 14),
            'bimonthly' => (int) ceil($termDays / 60),
            'quarterly' => (int) ceil($termDays / 90),
            'semiannual' => (int) ceil($termDays / 180),
            'yearly' => (int) ceil($termDays / 365),
            'single' => 1,
            default => (int) ceil($termDays / 30),
        });
    }

    public function expectedInstallments(Loan $loan): int
    {
        return $this->installmentsForPeriods((int) ($loan->term_value ?: 1), $loan->repayment_frequency, (int) $loan->term_days);
    }

    public function installmentsForPeriods(int $periods, string $frequency, ?int $termDays = null): int
    {
        if ($this->scheduleFrequency($frequency) === 'single') {
            return 1;
        }

        if ($periods > 0) {
            return $periods;
        }

        return $this->installmentCount($termDays ?: 30, $frequency);
    }

    public function termDays(int $value, string $unit): int
    {
        return max(1, $value) * match ($unit) {
            'weeks' => 7,
            'months' => 30,
            'years' => 365,
            default => 1,
        };
    }

    public function termDaysForPeriods(int $periods, string $frequency): int
    {
        $periods = max(1, $periods);

        return match ($this->scheduleFrequency($frequency)) {
            'daily' => $periods,
            'weekly' => $periods * 7,
            'biweekly' => $periods * 14,
            'bimonthly' => $periods * 60,
            'quarterly' => $periods * 90,
            'semiannual' => $periods * 180,
            'yearly' => $periods * 365,
            default => $periods * 30,
        };
    }

    public function termUnitForFrequency(string $frequency): string
    {
        return match ($this->scheduleFrequency($frequency)) {
            'daily' => 'days',
            'weekly', 'biweekly' => 'weeks',
            'yearly' => 'years',
            default => 'months',
        };
    }

    public function scheduleFrequency(string $frequency): string
    {
        return array_key_exists($frequency, self::CUSTOM_REPAYMENT_FREQUENCIES) ? $frequency : 'monthly';
    }

    public function normalInterestMethod(string $method): string
    {
        return match ($method) {
            'reducing', 'reducing_balance' => 'reducing_balance',
            'interest_only' => 'interest_only',
            'upfront' => 'upfront',
            'none' => 'none',
            'custom' => 'flat',
            default => 'flat',
        };
    }

    private function processingFee(float $principal, LoanProduct $product): float
    {
        $value = (float) ($product->processing_fee_value ?? 0);

        if ($value <= 0) {
            return 0.0;
        }

        return round(($product->processing_fee_type === 'percentage') ? $principal * ($value / 100) : $value, 2);
    }

    private function interestAmount(float $principal, float $rate, int $installments, string $method): float
    {
        if ($rate <= 0 || $this->normalInterestMethod($method) === 'none') {
            return 0.0;
        }

        if ($this->normalInterestMethod($method) !== 'reducing_balance') {
            return round($principal * ($rate / 100), 2);
        }

        $principalEach = $principal / max(1, $installments);
        $periodicRate = ($rate / 100) / max(1, $installments);
        $outstanding = $principal;
        $interest = 0.0;

        for ($i = 1; $i <= $installments; $i++) {
            $interest += round($outstanding * $periodicRate, 2);
            $outstanding = max(0, $outstanding - $principalEach);
        }

        return round($interest, 2);
    }

    private function principalDue(float $principal, int $installments, int $installment, string $method, float $allocated): float
    {
        if ($method === 'interest_only' && $installment < $installments) {
            return 0.0;
        }

        return $installment === $installments
            ? round($principal - $allocated, 2)
            : round($principal / $installments, 2);
    }

    private function interestDue(Loan $loan, int $installments, int $installment, string $method, float $allocated, float $outstandingPrincipal): float
    {
        $targetInterest = (float) $loan->interest_amount;

        return match ($method) {
            'none' => 0.0,
            'upfront' => $installment === 1 ? round($targetInterest, 2) : 0.0,
            'reducing_balance' => $installment === $installments
                ? round($targetInterest - $allocated, 2)
                : round($outstandingPrincipal * (((float) $loan->interest_rate / 100) / $installments), 2),
            default => $installment === $installments
                ? round($targetInterest - $allocated, 2)
                : round($targetInterest / $installments, 2),
        };
    }

    private function outstandingComponents(LoanRepaymentSchedule $schedule): array
    {
        $paid = (float) $schedule->paid_amount;
        $components = [
            'penalty_due' => (float) $schedule->penalty_due,
            'interest_due' => (float) $schedule->interest_due,
            'principal_due' => (float) $schedule->principal_due,
        ];

        $outstanding = [];

        foreach ($components as $key => $amount) {
            $covered = min($paid, $amount);
            $outstanding[$key] = round(max(0, $amount - $covered), 2);
            $paid = max(0, $paid - $amount);
        }

        return $outstanding;
    }

    private function nextDueDate(Carbon $start, string $frequency, int $installment, int $termDays): Carbon
    {
        if ($frequency === 'single') {
            return $start->copy()->addDays(max(1, $termDays));
        }

        return $this->addFrequency($start, $frequency, $installment, $termDays);
    }

    private function addFrequency(Carbon $date, string $frequency, int $periods, int $termDays): Carbon
    {
        if ($periods <= 0) {
            return $date->copy();
        }

        return match ($frequency) {
            'daily' => $date->copy()->addDays($periods),
            'weekly' => $date->copy()->addWeeks($periods),
            'biweekly' => $date->copy()->addWeeks($periods * 2),
            'bimonthly' => $date->copy()->addMonthsNoOverflow($periods * 2),
            'quarterly' => $date->copy()->addMonthsNoOverflow($periods * 3),
            'semiannual' => $date->copy()->addMonthsNoOverflow($periods * 6),
            'yearly' => $date->copy()->addYearsNoOverflow($periods),
            'single' => $date->copy()->addDays(max(1, $termDays)),
            default => $date->copy()->addMonthsNoOverflow($periods),
        };
    }
}
