<?php

namespace App\Services;

use App\Models\LoanProduct;
use Carbon\CarbonImmutable;
use InvalidArgumentException;

class LoanCalculationService
{
    public function __construct(private MoneyMath $money, private RepaymentPeriodService $periods) {}

    /** @param list<string|CarbonImmutable> $customDates @return array<string,mixed> */
    public function calculate(LoanProduct $product, string|int $principal, CarbonImmutable $startDate, array $customDates = []): array
    {
        $detail = $this->calculateDetailed($product, $principal, $startDate, $customDates);
        unset($detail['_principal_minor'], $detail['_interest_minor'], $detail['_fees_minor'], $detail['_total_minor'], $detail['_interest_parts_minor']);
        return $detail;
    }

    /** @param list<string|CarbonImmutable> $customDates @return array<string,mixed> */
    public function calculateDetailed(LoanProduct $product, string|int $principal, CarbonImmutable $startDate, array $customDates = []): array
    {
        $principalMinor = $this->money->toMinor($principal);
        $min = $this->money->toMinor((string) $product->minimum_amount); $max = $this->money->toMinor((string) $product->maximum_amount);
        if ($principalMinor < $min || $principalMinor > $max) throw new InvalidArgumentException('Principal is outside the loan product limits.');

        $dueDates = $this->periods->dueDates($product, $startDate, $customDates); $count = count($dueDates);
        if ($count < 1) throw new InvalidArgumentException('Loan must have at least one installment.');

        $interestParts = [];
        if ($product->interest_calculation_method === 'flat') {
            $interestMinor = $this->money->percentageOf($principalMinor, (string) $product->interest_rate);
            $interestParts = $this->money->allocate($interestMinor, $count);
        } elseif ($product->interest_calculation_method === 'fixed_amount') {
            $interestMinor = $this->money->toMinor((string) ($product->fixed_interest_amount ?? '0'));
            $interestParts = $this->money->allocate($interestMinor, $count);
        } elseif ($product->interest_calculation_method === 'reducing_balance') {
            $principalParts = $this->money->allocate($principalMinor, $count); $opening = $principalMinor; $interestMinor = 0;
            foreach ($principalParts as $part) {
                $periodInterest = $this->money->percentageOf($opening, (string) $product->interest_rate, $count);
                $interestParts[] = $periodInterest; $interestMinor += $periodInterest; $opening -= $part;
            }
        } else throw new InvalidArgumentException('Unsupported interest calculation method.');

        $processing = $this->fee($principalMinor, $product->processing_fee_type, (string) $product->processing_fee_value);
        $insurance = $this->fee($principalMinor, $product->insurance_fee_type, (string) $product->insurance_fee_value);
        $other = $this->money->toMinor((string) $product->other_charges);
        $mode = $product->processing_fee_payment_mode ?: 'financed';
        if (! array_key_exists($mode, LoanProduct::PROCESSING_FEE_PAYMENT_MODES)) throw new InvalidArgumentException('Unsupported processing fee payment mode.');

        $repayableProcessing = $mode === 'financed' ? $processing : 0;
        $cashFeeDue = $mode === 'cash' ? $processing : 0;
        $netDisbursement = $mode === 'deducted' ? $principalMinor - $processing : $principalMinor;
        if ($netDisbursement <= 0) throw new InvalidArgumentException('The processing fee leaves no amount available for disbursement.');

        $feesMinor = $repayableProcessing + $insurance + $other;
        $totalMinor = $principalMinor + $interestMinor + $feesMinor;

        return [
            'principal'=>$this->money->fromMinor($principalMinor),'interest'=>$this->money->fromMinor($interestMinor),
            'processing_fee'=>$this->money->fromMinor($processing),'processing_fee_payment_mode'=>$mode,
            'processing_fee_payment_mode_label'=>LoanProduct::PROCESSING_FEE_PAYMENT_MODES[$mode],
            'processing_fee_cash_due'=>$this->money->fromMinor($cashFeeDue),'net_disbursement'=>$this->money->fromMinor($netDisbursement),
            'insurance_fee'=>$this->money->fromMinor($insurance),'other_charges'=>$this->money->fromMinor($other),
            'fees'=>$this->money->fromMinor($feesMinor),'total_repayment'=>$this->money->fromMinor($totalMinor),
            'installment_count'=>$count,'interest_method'=>$product->interest_calculation_method,'repayment_frequency'=>$product->repayment_frequency,
            '_principal_minor'=>$principalMinor,'_interest_minor'=>$interestMinor,'_fees_minor'=>$feesMinor,'_total_minor'=>$totalMinor,'_interest_parts_minor'=>$interestParts,
        ];
    }

    private function fee(int $principalMinor, string $type, string $value): int
    {
        return match ($type) {
            'fixed' => $this->money->toMinor($value),
            'percentage' => $this->money->percentageOf($principalMinor, $value),
            default => throw new InvalidArgumentException('Unsupported fee type.'),
        };
    }
}
