<?php

namespace App\Support;

use App\Models\CurrencyExchange;
use App\Models\Saraf;
use App\Models\SarafPayment;
use App\Models\SarafPaymentAllocation;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class SarafAccountLedger
{
    /**
     * @return array{
     *     exchange_count: int,
     *     total_afn: string,
     *     total_dollar: string,
     *     receive_dollar: string,
     *     remain_dollar_on_saraf: string,
     * }
     */
    public static function summary(Saraf $saraf): array
    {
        $query = CurrencyExchange::query()->where('saraf_id', $saraf->id);

        return [
            'exchange_count' => (int) $query->clone()->count(),
            'total_afn' => self::formatMoney((float) $query->clone()->sum('total_afn')),
            'total_dollar' => self::formatMoney((float) $query->clone()->sum('total_dollar')),
            'receive_dollar' => self::formatMoney((float) $query->clone()->sum('receive_dollar')),
            'remain_dollar_on_saraf' => self::formatMoney((float) $query->clone()->sum('remain_dollar_on_saraf')),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public static function exchangeDetailPayload(CurrencyExchange $exchange): array
    {
        return [
            'id' => $exchange->id,
            'saraf_id' => $exchange->saraf_id,
            'exchange_date' => $exchange->exchange_date?->toDateString(),
            'total_afn' => $exchange->total_afn,
            'dollar_difference_price' => $exchange->dollar_difference_price,
            'dollar_price' => $exchange->dollar_price,
            'total_dollar' => $exchange->total_dollar,
            'receive_dollar' => $exchange->receive_dollar,
            'remain_dollar_on_saraf' => $exchange->remain_dollar_on_saraf,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public static function openExchangePayload(CurrencyExchange $exchange): array
    {
        $remain = round((float) $exchange->remain_dollar_on_saraf, 2);

        return [
            ...self::exchangeDetailPayload($exchange),
            'due_amount' => self::formatMoney($remain),
        ];
    }

    /**
     * @return Collection<int, array<string, mixed>>
     */
    public static function openExchangesForSaraf(Saraf $saraf, bool $oldestFirst = true): Collection
    {
        $query = CurrencyExchange::query()
            ->where('saraf_id', $saraf->id)
            ->where('remain_dollar_on_saraf', '>', 0.001);

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

        return $query->get()
            ->map(fn (CurrencyExchange $exchange): array => self::openExchangePayload($exchange))
            ->values();
    }

    public static function remainForExchange(CurrencyExchange $exchange): float
    {
        return round((float) $exchange->remain_dollar_on_saraf, 2);
    }

    public static function totalRemainForSaraf(Saraf $saraf): float
    {
        return round((float) CurrencyExchange::query()
            ->where('saraf_id', $saraf->id)
            ->sum('remain_dollar_on_saraf'), 2);
    }

    /**
     * @param  array{
     *     amount: float|int|string,
     *     paid_at: string,
     *     payment_method?: string|null,
     *     reference?: string|null,
     *     notes?: string|null,
     * }  $paymentData
     */
    public static function recordExchangePayment(
        Saraf $saraf,
        CurrencyExchange $exchange,
        array $paymentData,
    ): SarafPayment {
        if ((int) $exchange->saraf_id !== (int) $saraf->id) {
            throw new \InvalidArgumentException(__('Exchange does not belong to this saraf.'));
        }

        $amount = round((float) $paymentData['amount'], 2);
        $remain = self::remainForExchange($exchange);

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

        if ($amount > $remain + 0.001) {
            throw new \InvalidArgumentException(__('Payment cannot exceed the remaining USD on this exchange.'));
        }

        return self::recordPayment($saraf, [
            'payment_type' => SarafPaymentType::EXCHANGE,
            'currency_exchange_id' => $exchange->id,
            'amount' => $amount,
            'paid_at' => $paymentData['paid_at'],
            'payment_method' => $paymentData['payment_method'] ?? null,
            'reference' => $paymentData['reference'] ?? null,
            'notes' => $paymentData['notes'] ?? null,
        ], [
            ['currency_exchange_id' => $exchange->id, 'amount' => $amount],
        ]);
    }

    /**
     * @param  array{
     *     amount: float|int|string,
     *     paid_at: string,
     *     payment_method?: string|null,
     *     reference?: string|null,
     *     notes?: string|null,
     * }  $paymentData
     */
    public static function recordGeneralPayment(Saraf $saraf, array $paymentData): SarafPayment
    {
        $amount = round((float) $paymentData['amount'], 2);

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

        $allocations = self::buildFifoAllocations($saraf, $amount);

        if ($allocations === []) {
            throw new \InvalidArgumentException(__('No open exchange balance to pay.'));
        }

        $allocatedTotal = round(array_sum(array_column($allocations, 'amount')), 2);

        if ($amount > $allocatedTotal + 0.001) {
            throw new \InvalidArgumentException(__('Payment cannot exceed the total remaining USD on saraf.'));
        }

        return self::recordPayment($saraf, [
            'payment_type' => SarafPaymentType::GENERAL,
            'currency_exchange_id' => null,
            'amount' => $amount,
            'paid_at' => $paymentData['paid_at'],
            'payment_method' => $paymentData['payment_method'] ?? null,
            'reference' => $paymentData['reference'] ?? null,
            'notes' => $paymentData['notes'] ?? null,
        ], $allocations);
    }

    /**
     * @return list<array{currency_exchange_id: int, amount: float}>
     */
    public static function buildFifoAllocations(Saraf $saraf, float $amount): array
    {
        $remaining = round($amount, 2);
        $allocations = [];

        foreach (self::openExchangesForSaraf($saraf, oldestFirst: true) as $exchange) {
            if ($remaining <= 0.001) {
                break;
            }

            $due = round((float) $exchange['due_amount'], 2);

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

            $slice = round(min($due, $remaining), 2);
            $allocations[] = [
                'currency_exchange_id' => (int) $exchange['id'],
                'amount' => $slice,
            ];
            $remaining = round($remaining - $slice, 2);
        }

        return $allocations;
    }

    /**
     * @param  array{
     *     payment_type: string,
     *     currency_exchange_id?: int|null,
     *     amount: float|int|string,
     *     paid_at: string,
     *     payment_method?: string|null,
     *     reference?: string|null,
     *     notes?: string|null,
     * }  $paymentData
     * @param  list<array{currency_exchange_id: int, amount: float}>  $allocations
     */
    public static function recordPayment(
        Saraf $saraf,
        array $paymentData,
        array $allocations,
    ): SarafPayment {
        self::validateAllocations($saraf, $allocations);

        return DB::transaction(function () use ($saraf, $paymentData, $allocations): SarafPayment {
            /** @var SarafPayment $payment */
            $payment = SarafPayment::query()->create([
                'saraf_id' => $saraf->id,
                'payment_type' => $paymentData['payment_type'],
                'currency_exchange_id' => $paymentData['currency_exchange_id'] ?? null,
                'amount' => round((float) $paymentData['amount'], 2),
                'paid_at' => $paymentData['paid_at'],
                'payment_method' => $paymentData['payment_method'] ?? null,
                'reference' => $paymentData['reference'] ?? null,
                'notes' => $paymentData['notes'] ?? null,
            ]);

            foreach ($allocations as $allocation) {
                SarafPaymentAllocation::query()->create([
                    'saraf_payment_id' => $payment->id,
                    'currency_exchange_id' => $allocation['currency_exchange_id'],
                    'amount' => round((float) $allocation['amount'], 2),
                ]);

                $exchange = CurrencyExchange::query()
                    ->whereKey($allocation['currency_exchange_id'])
                    ->lockForUpdate()
                    ->firstOrFail();

                self::applyAmountToExchange($exchange, (float) $allocation['amount']);
            }

            return $payment->fresh(['allocations.currencyExchange']);
        });
    }

    /**
     * @param  list<array{currency_exchange_id: int, amount: float}>  $allocations
     */
    public static function validateAllocations(Saraf $saraf, array $allocations): void
    {
        if ($allocations === []) {
            throw new \InvalidArgumentException(__('At least one allocation is required.'));
        }

        foreach ($allocations as $allocation) {
            $exchange = CurrencyExchange::query()->find($allocation['currency_exchange_id']);

            if ($exchange === null || (int) $exchange->saraf_id !== (int) $saraf->id) {
                throw new \InvalidArgumentException(__('Exchange does not belong to this saraf.'));
            }

            $amount = round((float) $allocation['amount'], 2);
            $remain = self::remainForExchange($exchange);

            if ($amount <= 0 || $amount > $remain + 0.001) {
                throw new \InvalidArgumentException(__('Allocation cannot exceed the exchange remaining USD.'));
            }
        }
    }

    public static function applyAmountToExchange(CurrencyExchange $exchange, float $amount): void
    {
        $amount = round($amount, 2);
        $remain = self::remainForExchange($exchange);

        if ($amount <= 0 || $amount > $remain + 0.001) {
            throw new \InvalidArgumentException(__('Invalid payment amount for exchange.'));
        }

        $exchange->receive_dollar = self::formatMoney((float) $exchange->receive_dollar + $amount);
        $exchange->remain_dollar_on_saraf = self::formatMoney($remain - $amount);
        $exchange->save();
    }

    /**
     * @return array<string, mixed>
     */
    public static function paymentDetailPayload(SarafPayment $payment): array
    {
        $payment->loadMissing(['currencyExchange', 'allocations.currencyExchange']);

        $allocations = $payment->allocations
            ->map(fn (SarafPaymentAllocation $allocation): array => [
                'id' => $allocation->id,
                'currency_exchange_id' => $allocation->currency_exchange_id,
                'exchange_date' => $allocation->currencyExchange?->exchange_date?->toDateString(),
                'amount' => $allocation->amount,
            ])
            ->values()
            ->all();

        return [
            'id' => $payment->id,
            'payment_type' => $payment->payment_type,
            'currency_exchange_id' => $payment->currency_exchange_id,
            'exchange_date' => $payment->currencyExchange?->exchange_date?->toDateString(),
            'amount' => $payment->amount,
            'paid_at' => $payment->paid_at?->toDateString(),
            'payment_method' => $payment->payment_method,
            'reference' => $payment->reference,
            'notes' => $payment->notes,
            'allocations' => $allocations,
        ];
    }

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