<?php

namespace App\Support;

use App\Models\Customer;
use App\Models\CustomerLoanRepayment;
use App\Models\Sale;
use App\Models\SalePayment;
use Illuminate\Support\Collection;

class CustomerLedgerReport
{
    public const PAYMENT_GROUP_DATE = 'date';

    public const PAYMENT_GROUP_MONTH = 'month';

    public const PAYMENT_GROUP_YEAR = 'year';

    public const PAYMENT_GROUP_SALE = 'sale';

    /**
     * @return list<string>
     */
    public static function paymentGroupValues(): array
    {
        return [
            self::PAYMENT_GROUP_DATE,
            self::PAYMENT_GROUP_MONTH,
            self::PAYMENT_GROUP_YEAR,
            self::PAYMENT_GROUP_SALE,
        ];
    }

    /**
     * @param  array{
     *     customer_id?: int|null,
     *     date_from?: string|null,
     *     date_to?: string|null,
     *     payment_group?: string|null,
     * }  $filters
     * @return array{
     *     summary: array{
     *         currency: string,
     *         total_purchased: string,
     *         total_paid: string,
     *         purchase_outstanding: string,
     *         loan_outstanding: string,
     *         outstanding: string,
     *         purchase_count: int,
     *         payment_count: int,
     *     },
     *     purchases: list<array<string, mixed>>,
     *     payments: list<array<string, mixed>>,
     *     payment_group: string,
     * }
     */
    public static function build(array $filters): array
    {
        $customerId = isset($filters['customer_id']) ? (int) $filters['customer_id'] : null;
        $dateFrom = self::normalizeDate($filters['date_from'] ?? null);
        $dateTo = self::normalizeDate($filters['date_to'] ?? null);
        $paymentGroup = $filters['payment_group'] ?? self::PAYMENT_GROUP_DATE;

        if (! in_array($paymentGroup, self::paymentGroupValues(), true)) {
            $paymentGroup = self::PAYMENT_GROUP_DATE;
        }

        $purchases = self::purchases($customerId, $dateFrom, $dateTo);
        $paymentRows = self::paymentRows($customerId, $dateFrom, $dateTo);
        $payments = self::groupPayments($paymentRows, $paymentGroup);

        $totalPurchased = self::roundMoney(
            array_sum(array_map(fn (array $row): float => (float) $row['net_amount'], $purchases)),
        );
        $totalPaid = self::roundMoney(
            array_sum(array_map(fn (array $row): float => (float) $row['bill_amount'], $paymentRows)),
        );

        if ($customerId !== null) {
            $accountSummary = CustomerAccountLedger::summary(
                Customer::query()->findOrFail($customerId),
            );
            $purchaseOutstanding = (float) $accountSummary['purchase_outstanding'];
            $loanOutstanding = (float) $accountSummary['loan_outstanding'];
            $outstanding = (float) $accountSummary['outstanding'];
        } else {
            $purchaseOutstanding = max(0, $totalPurchased - $totalPaid);
            $loanOutstanding = 0.0;
            $outstanding = $purchaseOutstanding;
        }

        return [
            'summary' => [
                'currency' => SalePaymentLedger::baseCurrency(),
                'total_purchased' => self::formatMoney($totalPurchased),
                'total_paid' => self::formatMoney($totalPaid),
                'purchase_outstanding' => self::formatMoney($purchaseOutstanding),
                'loan_outstanding' => self::formatMoney($loanOutstanding),
                'outstanding' => self::formatMoney($outstanding),
                'purchase_count' => count($purchases),
                'payment_count' => count($paymentRows),
            ],
            'purchases' => $purchases,
            'payments' => $payments,
            'payment_group' => $paymentGroup,
        ];
    }

    /**
     * @return list<array<string, mixed>>
     */
    private static function purchases(?int $customerId, ?string $dateFrom, ?string $dateTo): array
    {
        return Sale::query()
            ->with([
                'customer:id,full_name',
                'order:id,order_number',
            ])
            ->where('type', SaleType::SALE)
            ->when($customerId !== null, fn ($query) => $query->where('customer_id', $customerId))
            ->when($dateFrom !== null, fn ($query) => $query->whereDate('sold_at', '>=', $dateFrom))
            ->when($dateTo !== null, fn ($query) => $query->whereDate('sold_at', '<=', $dateTo))
            ->orderByDesc('sold_at')
            ->orderByDesc('id')
            ->get()
            ->map(function (Sale $sale): array {
                $payment = CustomerAccountLedger::salePaymentPayload($sale);

                return [
                    'id' => $sale->id,
                    'sale_number' => $sale->sale_number,
                    'customer_id' => $sale->customer_id,
                    'customer_name' => $sale->customer?->full_name,
                    'order_number' => $sale->order?->order_number,
                    'sold_at' => $sale->sold_at?->toDateString(),
                    'net_amount' => (string) $sale->net_amount,
                    'paid_amount' => (string) $payment['paid_amount'],
                    'due_amount' => (string) $payment['due_amount'],
                    'payment_status' => $payment['payment_status'],
                    'currency' => SalePaymentLedger::baseCurrency(),
                ];
            })
            ->values()
            ->all();
    }

    /**
     * @return list<array<string, mixed>>
     */
    private static function paymentRows(?int $customerId, ?string $dateFrom, ?string $dateTo): array
    {
        $salePayments = SalePayment::query()
            ->with([
                'sale:id,sale_number,customer_id,sold_at,type',
                'sale.customer:id,full_name',
            ])
            ->whereHas('sale', function ($query) use ($customerId): void {
                $query->where('type', SaleType::SALE);

                if ($customerId !== null) {
                    $query->where('customer_id', $customerId);
                }
            })
            ->when($dateFrom !== null, fn ($query) => $query->whereDate('paid_at', '>=', $dateFrom))
            ->when($dateTo !== null, fn ($query) => $query->whereDate('paid_at', '<=', $dateTo))
            ->orderByDesc('paid_at')
            ->orderByDesc('id')
            ->get()
            ->map(fn (SalePayment $payment): array => [
                'id' => 'sale-'.$payment->id,
                'source' => 'sale_payment',
                'payment_type' => $payment->payment_type ?? CustomerPaymentType::SALE,
                'sale_id' => $payment->sale_id,
                'loan_id' => null,
                'sale_number' => $payment->sale?->sale_number,
                'customer_id' => $payment->sale?->customer_id,
                'customer_name' => $payment->sale?->customer?->full_name,
                'paid_at' => $payment->paid_at?->toDateString(),
                'currency' => $payment->currency,
                'bill_currency' => SalePaymentLedger::baseCurrency(),
                'amount' => (string) $payment->amount,
                'bill_amount' => NumberFormat::money($payment->bill_amount),
                'display_amount' => NumberFormat::money($payment->bill_amount),
                'payment_method' => $payment->payment_method,
                'reference' => $payment->reference,
                'notes' => $payment->notes,
            ]);

        $loanRepayments = CustomerLoanRepayment::query()
            ->with([
                'loan:id,loan_number,customer_id',
                'customer:id,full_name',
            ])
            ->when($customerId !== null, fn ($query) => $query->where('customer_id', $customerId))
            ->when($dateFrom !== null, fn ($query) => $query->whereDate('paid_at', '>=', $dateFrom))
            ->when($dateTo !== null, fn ($query) => $query->whereDate('paid_at', '<=', $dateTo))
            ->orderByDesc('paid_at')
            ->orderByDesc('id')
            ->get()
            ->map(function (CustomerLoanRepayment $repayment): array {
                $amount = NumberFormat::money($repayment->amount);
                $loanNumber = $repayment->loan?->loan_number;

                return [
                    'id' => 'loan-'.$repayment->id,
                    'source' => 'loan_repayment',
                    'payment_type' => 'loan',
                    'sale_id' => null,
                    'loan_id' => $repayment->customer_loan_id,
                    'sale_number' => null,
                    'loan_number' => $loanNumber,
                    'customer_id' => $repayment->customer_id,
                    'customer_name' => $repayment->customer?->full_name,
                    'paid_at' => $repayment->paid_at?->toDateString(),
                    'currency' => SalePaymentLedger::baseCurrency(),
                    'bill_currency' => SalePaymentLedger::baseCurrency(),
                    'amount' => $amount,
                    'bill_amount' => $amount,
                    'display_amount' => $amount,
                    'payment_method' => $repayment->payment_method,
                    'reference' => $repayment->reference,
                    'notes' => $repayment->notes,
                ];
            });

        return $salePayments
            ->concat($loanRepayments)
            ->sort(function (array $left, array $right): int {
                $dateCompare = strcmp((string) ($right['paid_at'] ?? ''), (string) ($left['paid_at'] ?? ''));

                if ($dateCompare !== 0) {
                    return $dateCompare;
                }

                return strcmp((string) ($right['id'] ?? ''), (string) ($left['id'] ?? ''));
            })
            ->values()
            ->all();
    }

    /**
     * @param  list<array<string, mixed>>  $rows
     * @return list<array<string, mixed>>
     */
    private static function groupPayments(array $rows, string $paymentGroup): array
    {
        if ($paymentGroup === self::PAYMENT_GROUP_DATE) {
            return $rows;
        }

        $collection = collect($rows);

        if ($paymentGroup === self::PAYMENT_GROUP_SALE) {
            return $collection
                ->groupBy(function (array $row): string {
                    if (($row['source'] ?? null) === 'loan_repayment' || ($row['loan_id'] ?? null) !== null) {
                        return 'loan:'.(string) ($row['loan_id'] ?? $row['id']);
                    }

                    return 'sale:'.(string) ($row['sale_id'] ?? $row['id']);
                })
                ->map(function (Collection $group, string $key): array {
                    $first = $group->first();
                    $isLoan = str_starts_with($key, 'loan:');
                    $documentNumber = $isLoan
                        ? ($first['loan_number'] ?? $first['sale_number'] ?? null)
                        : ($first['sale_number'] ?? null);

                    return [
                        'key' => $key,
                        'sale_id' => $isLoan ? null : (int) ($first['sale_id'] ?? 0),
                        'loan_id' => $isLoan ? (int) ($first['loan_id'] ?? 0) : null,
                        'sale_number' => $isLoan ? null : $documentNumber,
                        'loan_number' => $isLoan ? $documentNumber : null,
                        'customer_id' => $first['customer_id'] ?? null,
                        'customer_name' => $first['customer_name'] ?? null,
                        'period_label' => $documentNumber ?? '—',
                        'payment_type' => $isLoan ? 'loan' : ($first['payment_type'] ?? CustomerPaymentType::SALE),
                        'source' => $isLoan ? 'loan_repayment' : 'sale_payment',
                        'payment_count' => $group->count(),
                        'bill_amount' => self::formatMoney(
                            self::roundMoney($group->sum(fn (array $row): float => (float) $row['bill_amount'])),
                        ),
                        'paid_from' => $group->min('paid_at'),
                        'paid_to' => $group->max('paid_at'),
                        'currency' => $first['currency'] ?? SalePaymentLedger::baseCurrency(),
                    ];
                })
                ->sortByDesc('paid_to')
                ->values()
                ->all();
        }

        $periodKey = match ($paymentGroup) {
            self::PAYMENT_GROUP_MONTH => fn (array $row): string => substr((string) ($row['paid_at'] ?? ''), 0, 7),
            self::PAYMENT_GROUP_YEAR => fn (array $row): string => substr((string) ($row['paid_at'] ?? ''), 0, 4),
            default => fn (array $row): string => (string) ($row['paid_at'] ?? ''),
        };

        return $collection
            ->groupBy($periodKey)
            ->map(function (Collection $group, string $key): array {
                $first = $group->first();

                return [
                    'key' => $key,
                    'period_label' => $key !== '' ? $key : '—',
                    'payment_count' => $group->count(),
                    'bill_amount' => self::formatMoney(
                        self::roundMoney($group->sum(fn (array $row): float => (float) $row['bill_amount'])),
                    ),
                    'paid_from' => $group->min('paid_at'),
                    'paid_to' => $group->max('paid_at'),
                    'currency' => $first['currency'] ?? SalePaymentLedger::baseCurrency(),
                    'customer_name' => $first['customer_name'] ?? null,
                ];
            })
            ->sortByDesc('period_label')
            ->values()
            ->all();
    }

    private static function normalizeDate(?string $value): ?string
    {
        if ($value === null || trim($value) === '') {
            return null;
        }

        return $value;
    }

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

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