<?php

namespace App\Support;

use App\Models\Expense;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

class ExpenseReport
{
    /**
     * @param  array{
     *     date_from?: string|null,
     *     date_to?: string|null,
     *     expense_category_id?: int|null,
     *     payment_method?: string|null,
     *     status?: string|null
     * }  $filters
     * @return array<string, mixed>
     */
    public static function build(array $filters): array
    {
        $query = self::baseQuery($filters);

        $rows = (clone $query)
            ->with(['category:id,name,slug', 'creator:id,name'])
            ->orderByDesc('expense_date')
            ->orderByDesc('id')
            ->get();

        $countable = $rows->filter(fn (Expense $expense): bool => $expense->countsTowardTotals());

        $total = (float) $countable->sum('amount_original');
        $paid = (float) $countable->where('status', ExpenseStatus::PAID)->sum('amount_original');
        $pending = (float) $countable->where('status', ExpenseStatus::PENDING)->sum('amount_original');
        $records = $countable->count();
        $average = $records > 0 ? round($total / $records, 2) : 0.0;

        return [
            'summary' => [
                'total_afn' => self::formatAmount($total),
                'paid_afn' => self::formatAmount($paid),
                'pending_afn' => self::formatAmount($pending),
                'records' => $records,
                'average_afn' => self::formatAmount($average),
            ],
            'by_category' => self::groupByCategory($countable, $total),
            'by_payment' => self::groupByPayment($countable, $total),
            'by_month' => self::groupByPeriod($countable, 'month'),
            'by_year' => self::groupByPeriod($countable, 'year'),
            'by_day' => self::groupByPeriod($countable, 'day'),
            'expenses' => $rows->map(fn (Expense $expense): array => self::expensePayload($expense))->values()->all(),
            'period_total_afn' => self::formatAmount($total),
        ];
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    public static function baseQuery(array $filters): Builder
    {
        $query = Expense::query()
            ->where('status', '!=', ExpenseStatus::CANCELLED);

        BranchScope::apply($query);

        if (! empty($filters['date_from'])) {
            $query->whereDate('expense_date', '>=', $filters['date_from']);
        }

        if (! empty($filters['date_to'])) {
            $query->whereDate('expense_date', '<=', $filters['date_to']);
        }

        if (! empty($filters['expense_category_id'])) {
            $query->where('expense_category_id', (int) $filters['expense_category_id']);
        }

        if (! empty($filters['payment_method'])) {
            $query->where('payment_method', (string) $filters['payment_method']);
        }

        if (! empty($filters['status'])) {
            $query->where('status', (string) $filters['status']);
        }

        return $query;
    }

    /**
     * @return array<string, string|null>
     */
    public static function defaultFilters(): array
    {
        $now = Carbon::now();

        return [
            'date_from' => $now->copy()->startOfMonth()->toDateString(),
            'date_to' => $now->copy()->endOfMonth()->toDateString(),
            'expense_category_id' => null,
            'payment_method' => null,
            'status' => null,
        ];
    }

    /**
     * @param  Collection<int, Expense>  $expenses
     * @return list<array<string, mixed>>
     */
    private static function groupByCategory(Collection $expenses, float $total): array
    {
        return $expenses
            ->groupBy('expense_category_id')
            ->map(function (Collection $group) use ($total): array {
                /** @var Expense $first */
                $first = $group->first();
                $amount = (float) $group->sum('amount_original');

                return [
                    'category_id' => $first->expense_category_id,
                    'category_slug' => $first->category?->slug,
                    'category_name' => $first->category?->displayName() ?? __('Uncategorized'),
                    'count' => $group->count(),
                    'amount_afn' => self::formatAmount($amount),
                    'share_percent' => $total > 0 ? round(($amount / $total) * 100, 2) : 0,
                ];
            })
            ->sortByDesc(fn (array $row): float => (float) str_replace(',', '', $row['amount_afn']))
            ->values()
            ->all();
    }

    /**
     * @param  Collection<int, Expense>  $expenses
     * @return list<array<string, mixed>>
     */
    private static function groupByPayment(Collection $expenses, float $total): array
    {
        $labels = ExpensePaymentMethod::labels();

        return $expenses
            ->groupBy(fn (Expense $expense): string => (string) ($expense->payment_method ?? ''))
            ->map(function (Collection $group, string $method) use ($total, $labels): array {
                $amount = (float) $group->sum('amount_original');

                return [
                    'payment_method' => $method,
                    'payment_label' => $labels[$method] ?? ($method !== '' ? $method : __('Unknown')),
                    'count' => $group->count(),
                    'amount_afn' => self::formatAmount($amount),
                    'share_percent' => $total > 0 ? round(($amount / $total) * 100, 2) : 0,
                ];
            })
            ->sortByDesc(fn (array $row): float => (float) str_replace(',', '', $row['amount_afn']))
            ->values()
            ->all();
    }

    /**
     * @param  Collection<int, Expense>  $expenses
     * @return list<array<string, mixed>>
     */
    private static function groupByPeriod(Collection $expenses, string $period): array
    {
        return $expenses
            ->groupBy(function (Expense $expense) use ($period): string {
                $date = Carbon::parse($expense->expense_date);

                return match ($period) {
                    'year' => (string) ExpenseDates::jalaliYear($date),
                    'month' => ExpenseDates::jalaliYearMonth($date),
                    default => $date->toDateString(),
                };
            })
            ->map(function (Collection $group, string $key) use ($period): array {
                return [
                    'period' => $key,
                    'period_label' => ExpenseDates::formatPeriodLabel($period, $key),
                    'count' => $group->count(),
                    'amount_afn' => self::formatAmount((float) $group->sum('amount_original')),
                ];
            })
            ->sortKeysDesc()
            ->values()
            ->all();
    }

    /**
     * @return array<string, mixed>
     */
    public static function expensePayload(Expense $expense): array
    {
        return [
            'id' => $expense->id,
            'expense_day_id' => $expense->expense_day_id,
            'voucher_number' => $expense->voucher_number,
            'expense_date' => $expense->expense_date?->toDateString(),
            'expense_date_display' => ExpenseDates::formatDate($expense->expense_date),
            'category_id' => $expense->expense_category_id,
            'category_slug' => $expense->category?->slug,
            'category_name' => $expense->category?->displayName(),
            'description' => $expense->description,
            'vendor_paid_to' => $expense->vendor_paid_to,
            'amount_afn' => self::formatAmount((float) $expense->amount_original),
            'amount_original' => (string) $expense->amount_original,
            'payment_method' => $expense->payment_method,
            'payment_label' => ExpensePaymentMethod::labels()[$expense->payment_method] ?? $expense->payment_method,
            'status' => $expense->status,
            'status_label' => ExpenseStatus::labels()[$expense->status] ?? $expense->status,
            'created_by_name' => $expense->creator?->name,
            'is_cancelled' => $expense->isCancelled(),
        ];
    }

    public static function formatAmount(float $amount): string
    {
        return NumberFormat::money($amount, thousandsSeparator: true);
    }
}
