<?php

namespace App\Support;

use App\Models\Customer;
use App\Models\CustomerLoan;
use App\Models\CustomerLoanRepayment;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class CustomerLoanLedger
{
    /**
     * @return array{
     *     currency: string,
     *     total_loaned: string,
     *     total_repaid: string,
     *     outstanding: string,
     * }
     */
    public static function summary(Customer $customer): array
    {
        $currency = SalePaymentLedger::baseCurrency();
        $loans = CustomerLoan::query()
            ->where('customer_id', $customer->id)
            ->where('is_active', true)
            ->get();

        $totalLoaned = self::roundMoney((float) $loans->sum('amount'));
        $totalRepaid = self::roundMoney(self::totalRepaidForLoans($loans->pluck('id')->all()));
        $outstanding = self::roundMoney(
            $loans->sum(fn (CustomerLoan $loan): float => self::dueAmountForLoan($loan)),
        );

        return [
            'currency' => $currency,
            'total_loaned' => self::formatMoney($totalLoaned),
            'total_repaid' => self::formatMoney($totalRepaid),
            'outstanding' => self::formatMoney($outstanding),
        ];
    }

    public static function outstandingAmount(Customer $customer): float
    {
        return (float) self::summary($customer)['outstanding'];
    }

    public static function repaidAmountForLoan(CustomerLoan $loan): float
    {
        return self::roundMoney((float) CustomerLoanRepayment::query()
            ->where('customer_loan_id', $loan->id)
            ->sum('amount'));
    }

    public static function dueAmountForLoan(CustomerLoan $loan): float
    {
        if (! $loan->is_active) {
            return 0.0;
        }

        return max(0, self::roundMoney((float) $loan->amount - self::repaidAmountForLoan($loan)));
    }

    /**
     * @return Collection<int, array{
     *     id: int,
     *     loan_number: string,
     *     loan_date: string|null,
     *     amount: string,
     *     repaid_amount: float,
     *     due_amount: float,
     *     payment_status: string,
     * }>
     */
    public static function openLoansForCustomer(
        Customer $customer,
        bool $oldestFirst = false,
    ): Collection {
        $query = CustomerLoan::query()
            ->where('customer_id', $customer->id)
            ->where('is_active', true);

        if ($oldestFirst) {
            $query->orderBy('loan_date')->orderBy('id');
        } else {
            $query->orderByDesc('loan_date')->orderByDesc('id');
        }

        return $query->get()
            ->map(fn (CustomerLoan $loan): array => self::loanListPayload($loan))
            ->filter(fn (array $row): bool => $row['due_amount'] > 0.001)
            ->values();
    }

    /**
     * @return array<string, mixed>
     */
    public static function loanDetailPayload(CustomerLoan $loan): array
    {
        $loan->loadMissing(['customer:id,full_name,contact_number', 'creator:id,name']);

        $repaid = self::repaidAmountForLoan($loan);
        $due = self::dueAmountForLoan($loan);

        return [
            'id' => $loan->id,
            'customer_id' => $loan->customer_id,
            'customer_name' => $loan->customer?->full_name,
            'loan_number' => $loan->loan_number,
            'loan_date' => $loan->loan_date?->toDateString(),
            'amount' => (string) $loan->amount,
            'amount_afn' => self::formatMoney((float) $loan->amount),
            'repaid_amount' => (string) $repaid,
            'repaid_amount_afn' => self::formatMoney($repaid),
            'due_amount' => (string) $due,
            'due_amount_afn' => self::formatMoney($due),
            'payment_status' => self::resolveStatus((float) $loan->amount, $repaid, $loan->is_active),
            'payment_method' => $loan->payment_method,
            'reference' => $loan->reference,
            'notes' => $loan->notes,
            'is_active' => $loan->is_active,
            'created_by_name' => $loan->creator?->name,
            'can_edit' => $loan->is_active && $repaid <= 0.001,
            'can_repay' => $loan->is_active && $due > 0.001,
        ];
    }

    /**
     * @param  array{
     *     paid_at: string,
     *     amount: float|int|string,
     *     payment_method?: string|null,
     *     reference?: string|null,
     *     notes?: string|null,
     * }  $repaymentData
     */
    public static function recordRepayment(CustomerLoan $loan, array $repaymentData): CustomerLoanRepayment
    {
        if (! $loan->is_active) {
            throw new \InvalidArgumentException(__('Inactive loans cannot receive repayments.'));
        }

        $amount = self::roundMoney((float) $repaymentData['amount']);
        $due = self::dueAmountForLoan($loan);

        if ($amount <= 0.001) {
            throw new \InvalidArgumentException(__('Repayment amount must be greater than zero.'));
        }

        if ($amount > $due + 0.001) {
            throw new \InvalidArgumentException(__('Repayment amount exceeds the loan due amount.'));
        }

        return DB::transaction(function () use ($loan, $repaymentData, $amount): CustomerLoanRepayment {
            return CustomerLoanRepayment::query()->create([
                'customer_loan_id' => $loan->id,
                'customer_id' => $loan->customer_id,
                'paid_at' => $repaymentData['paid_at'],
                'amount' => $amount,
                'payment_method' => $repaymentData['payment_method'] ?? null,
                'reference' => $repaymentData['reference'] ?? null,
                'notes' => $repaymentData['notes'] ?? null,
            ]);
        });
    }

    /**
     * @param  array{
     *     paid_at: string,
     *     amount: float|int|string,
     *     payment_method?: string|null,
     *     reference?: string|null,
     *     notes?: string|null,
     * }  $repaymentData
     * @return list<CustomerLoanRepayment>
     */
    public static function recordOverallRepayment(Customer $customer, array $repaymentData): array
    {
        $remaining = self::roundMoney((float) $repaymentData['amount']);

        if ($remaining <= 0.001) {
            throw new \InvalidArgumentException(__('Repayment amount must be greater than zero.'));
        }

        $allocations = self::buildFifoAllocations($customer, $remaining);

        if ($allocations === []) {
            throw new \InvalidArgumentException(__('No open loans for this customer.'));
        }

        return DB::transaction(function () use ($customer, $repaymentData, $allocations): array {
            $payments = [];

            foreach ($allocations as $allocation) {
                /** @var CustomerLoan $loan */
                $loan = CustomerLoan::query()
                    ->where('customer_id', $customer->id)
                    ->where('is_active', true)
                    ->lockForUpdate()
                    ->findOrFail($allocation['loan_id']);

                $payments[] = self::recordRepayment($loan, [
                    ...$repaymentData,
                    'amount' => $allocation['amount'],
                ]);
            }

            return $payments;
        });
    }

    /**
     * @return list<array{loan_id: int, amount: float}>
     */
    public static function buildFifoAllocations(Customer $customer, float $amount): array
    {
        $remaining = self::roundMoney($amount);
        $allocations = [];

        foreach (self::openLoansForCustomer($customer, oldestFirst: true) as $loan) {
            if ($remaining <= 0.001) {
                break;
            }

            $due = (float) $loan['due_amount'];

            if ($due <= 0.001) {
                continue;
            }

            $slice = self::roundMoney(min($due, $remaining));
            $allocations[] = [
                'loan_id' => (int) $loan['id'],
                'amount' => $slice,
            ];
            $remaining = self::roundMoney($remaining - $slice);
        }

        return $allocations;
    }

    public static function generateLoanNumber(string $loanDate): string
    {
        $prefix = 'LN-'.str_replace('-', '', $loanDate).'-';

        $latest = CustomerLoan::query()
            ->where('loan_number', 'like', $prefix.'%')
            ->orderByDesc('loan_number')
            ->value('loan_number');

        $sequence = 1;

        if (is_string($latest) && str_starts_with($latest, $prefix)) {
            $suffix = substr($latest, strlen($prefix));
            $sequence = max(1, (int) $suffix + 1);
        }

        return $prefix.str_pad((string) $sequence, 3, '0', STR_PAD_LEFT);
    }

    /**
     * @return array{
     *     id: int,
     *     loan_number: string,
     *     loan_date: string|null,
     *     amount: string,
     *     repaid_amount: float,
     *     due_amount: float,
     *     payment_status: string,
     * }
     */
    private static function loanListPayload(CustomerLoan $loan): array
    {
        $repaid = self::repaidAmountForLoan($loan);
        $due = self::dueAmountForLoan($loan);

        return [
            'id' => $loan->id,
            'loan_number' => $loan->loan_number,
            'loan_date' => $loan->loan_date?->toDateString(),
            'amount' => (string) $loan->amount,
            'repaid_amount' => $repaid,
            'due_amount' => $due,
            'payment_status' => self::resolveStatus((float) $loan->amount, $repaid, $loan->is_active),
        ];
    }

    /**
     * @param  list<int>  $loanIds
     */
    private static function totalRepaidForLoans(array $loanIds): float
    {
        if ($loanIds === []) {
            return 0.0;
        }

        return (float) CustomerLoanRepayment::query()
            ->whereIn('customer_loan_id', $loanIds)
            ->sum('amount');
    }

    private static function resolveStatus(float $amount, float $repaid, bool $isActive): string
    {
        if (! $isActive) {
            return 'cancelled';
        }

        if ($repaid <= 0.001) {
            return SalePaymentStatus::OPEN;
        }

        if ($repaid + 0.001 >= $amount) {
            return SalePaymentStatus::PAID;
        }

        return SalePaymentStatus::PARTIAL;
    }

    private static function roundMoney(float $amount): float
    {
        return round($amount, 2);
    }

    private static function formatMoney(float $amount): string
    {
        return NumberFormat::money($amount);
    }
}
