<?php

namespace App\Support;

use App\Models\Expense;
use App\Models\OwnerWithdrawal;
use App\Models\SalePayment;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;

class ManagerCashLedger
{
    /**
     * @return array{
     *     previous_balance: float,
     *     previous_balance_afn: string,
     *     today_sales: float,
     *     today_sales_afn: string,
     *     today_expenses: float,
     *     today_expenses_afn: string,
     *     today_net_cash: float,
     *     today_net_cash_afn: string,
     *     total_available_cash: float,
     *     total_available_cash_afn: string,
     *     total_owner_withdrawals: float,
     *     total_owner_withdrawals_afn: string,
     *     remaining_cash_balance: float,
     *     remaining_cash_balance_afn: string,
     *     today_withdrawals: float,
     *     today_withdrawals_afn: string,
     *     as_of: string,
     * }
     */
    public static function summary(?string $asOf = null): array
    {
        $today = $asOf !== null
            ? Carbon::parse($asOf)->toDateString()
            : Carbon::now()->toDateString();

        $history = self::buildDailyRows(null, $today);
        $previousBalance = 0.0;
        $todayRow = null;

        foreach ($history as $row) {
            if ($row['date'] === $today) {
                $todayRow = $row;

                continue;
            }

            if ($row['date'] < $today) {
                $previousBalance = $row['running_balance'];
                break;
            }
        }

        if ($todayRow === null) {
            $todayTotals = self::totalsByDate()->get($today, self::emptyDayTotals());
            $todayNet = Money::toFloat(bcsub(
                Money::round($todayTotals['sales_received']),
                Money::round($todayTotals['company_expenses']),
                2,
            ));
            $todayWithdrawals = Money::toFloat($todayTotals['owner_withdrawals']);
            $todayRow = [
                'sales_received' => Money::toFloat($todayTotals['sales_received']),
                'company_expenses' => Money::toFloat($todayTotals['company_expenses']),
                'net_cash' => $todayNet,
                'owner_withdrawals' => $todayWithdrawals,
                'running_balance' => Money::toFloat(bcsub(
                    Money::round(bcadd(Money::round($previousBalance), Money::round($todayNet), 2)),
                    Money::round($todayWithdrawals),
                    2,
                )),
            ];
        }

        $totalAvailableCash = Money::toFloat(bcadd(
            Money::round($previousBalance),
            Money::round($todayRow['net_cash']),
            2,
        ));
        $totalOwnerWithdrawals = Money::toFloat(
            self::totalsByDate()
                ->sum(fn (array $row): float => (float) $row['owner_withdrawals']),
        );
        $remainingCashBalance = Money::toFloat($todayRow['running_balance']);

        return [
            'previous_balance' => Money::toFloat($previousBalance),
            'previous_balance_afn' => ExpenseReport::formatAmount($previousBalance),
            'today_sales' => Money::toFloat($todayRow['sales_received']),
            'today_sales_afn' => ExpenseReport::formatAmount((float) $todayRow['sales_received']),
            'today_expenses' => Money::toFloat($todayRow['company_expenses']),
            'today_expenses_afn' => ExpenseReport::formatAmount((float) $todayRow['company_expenses']),
            'today_net_cash' => Money::toFloat($todayRow['net_cash']),
            'today_net_cash_afn' => ExpenseReport::formatAmount((float) $todayRow['net_cash']),
            'total_available_cash' => $totalAvailableCash,
            'total_available_cash_afn' => ExpenseReport::formatAmount($totalAvailableCash),
            'total_owner_withdrawals' => $totalOwnerWithdrawals,
            'total_owner_withdrawals_afn' => ExpenseReport::formatAmount($totalOwnerWithdrawals),
            'remaining_cash_balance' => $remainingCashBalance,
            'remaining_cash_balance_afn' => ExpenseReport::formatAmount($remainingCashBalance),
            'today_withdrawals' => Money::toFloat($todayRow['owner_withdrawals']),
            'today_withdrawals_afn' => ExpenseReport::formatAmount((float) $todayRow['owner_withdrawals']),
            'as_of' => $today,
        ];
    }

    /**
     * @return array{
     *     current_total: float,
     *     current_total_afn: string,
     *     days: list<array<string, mixed>>,
     * }
     */
    public static function pendingWithManager(?string $dateFrom = null, ?string $dateTo = null): array
    {
        $days = self::buildDailyRows($dateFrom, $dateTo)
            ->filter(function (array $row): bool {
                return $row['remaining_with_manager'] > 0.00001
                    || (
                        $row['net_cash'] > 0.00001
                        && $row['net_cash'] > $row['owner_withdrawals'] + 0.00001
                    );
            })
            ->values()
            ->all();

        $currentTotal = self::remainingBalance($dateTo);

        return [
            'current_total' => $currentTotal,
            'current_total_afn' => ExpenseReport::formatAmount($currentTotal),
            'days' => $days,
        ];
    }

    /**
     * @return Collection<int, array<string, mixed>>
     */
    public static function dailyHistory(?string $dateFrom = null, ?string $dateTo = null): Collection
    {
        return self::buildDailyRows($dateFrom, $dateTo);
    }

    /**
     * @param  array{date_from?: string|null, date_to?: string|null, type?: string|null}  $filters
     * @return Collection<int, array<string, mixed>>
     */
    public static function transactions(array $filters = []): Collection
    {
        $dateFrom = $filters['date_from'] ?? null;
        $dateTo = $filters['date_to'] ?? null;
        $type = $filters['type'] ?? 'all';
        $items = collect();

        if ($type === 'all' || $type === 'sales_receipt') {
            $paymentsQuery = SalePayment::query()
                ->whereHas('sale', fn ($query) => $query->where('type', SaleType::SALE))
                ->orderByDesc('paid_at')
                ->orderByDesc('id');

            if ($dateFrom) {
                $paymentsQuery->whereDate('paid_at', '>=', $dateFrom);
            }
            if ($dateTo) {
                $paymentsQuery->whereDate('paid_at', '<=', $dateTo);
            }

            foreach ($paymentsQuery->get(['id', 'bill_amount', 'paid_at']) as $payment) {
                $amount = Money::toFloat($payment->bill_amount);
                $items->push([
                    'id' => 'sale-'.$payment->id,
                    'date' => $payment->paid_at?->toDateString(),
                    'type' => 'sales_receipt',
                    'description' => __('Sales receipt'),
                    'amount' => $amount,
                    'amount_afn' => ExpenseReport::formatAmount($amount),
                    'direction' => 'in',
                ]);
            }
        }

        if ($type === 'all' || $type === 'company_expense') {
            $expensesQuery = Expense::query()
                ->with(['category:id,name'])
                ->where('status', ExpenseStatus::PAID)
                ->orderByDesc('expense_date')
                ->orderByDesc('id');

            BranchScope::apply($expensesQuery);

            if ($dateFrom) {
                $expensesQuery->whereDate('expense_date', '>=', $dateFrom);
            }
            if ($dateTo) {
                $expensesQuery->whereDate('expense_date', '<=', $dateTo);
            }

            foreach ($expensesQuery->get([
                'id',
                'expense_date',
                'amount_original',
                'expense_category_id',
                'voucher_number',
            ]) as $expense) {
                $amount = Money::toFloat($expense->amount_original);
                $label = $expense->category?->name
                    ?? $expense->voucher_number
                    ?? __('Company expense');

                $items->push([
                    'id' => 'expense-'.$expense->id,
                    'date' => $expense->expense_date?->toDateString(),
                    'type' => 'company_expense',
                    'description' => $label,
                    'amount' => $amount,
                    'amount_afn' => ExpenseReport::formatAmount($amount),
                    'direction' => 'out',
                ]);
            }
        }

        if ($type === 'all' || $type === 'owner_withdrawal') {
            $withdrawalsQuery = OwnerWithdrawal::query()
                ->orderByDesc('withdrawn_on')
                ->orderByDesc('id');

            BranchScope::apply($withdrawalsQuery);

            if ($dateFrom) {
                $withdrawalsQuery->whereDate('withdrawn_on', '>=', $dateFrom);
            }
            if ($dateTo) {
                $withdrawalsQuery->whereDate('withdrawn_on', '<=', $dateTo);
            }

            foreach ($withdrawalsQuery->get() as $withdrawal) {
                $amount = Money::toFloat($withdrawal->amount);
                $items->push([
                    'id' => 'withdrawal-'.$withdrawal->id,
                    'date' => $withdrawal->withdrawn_on?->toDateString(),
                    'type' => 'owner_withdrawal',
                    'description' => $withdrawal->received_by_name
                        ? __('Owner withdrawal (:name)', ['name' => $withdrawal->received_by_name])
                        : __('Owner withdrawal'),
                    'amount' => $amount,
                    'amount_afn' => ExpenseReport::formatAmount($amount),
                    'direction' => 'out',
                ]);
            }
        }

        return $items
            ->sortByDesc(fn (array $row): string => ($row['date'] ?? '').'-'.$row['id'])
            ->values();
    }

    /**
     * @return Collection<int, array<string, mixed>>
     */
    public static function withdrawalsForIndex(?string $dateFrom = null, ?string $dateTo = null): Collection
    {
        $query = OwnerWithdrawal::query()
            ->with(['creator:id,name'])
            ->orderByDesc('withdrawn_on')
            ->orderByDesc('id');

        BranchScope::apply($query);

        if ($dateFrom) {
            $query->whereDate('withdrawn_on', '>=', $dateFrom);
        }
        if ($dateTo) {
            $query->whereDate('withdrawn_on', '<=', $dateTo);
        }

        return $query->get()->map(fn (OwnerWithdrawal $withdrawal): array => self::withdrawalPayload($withdrawal));
    }

    /**
     * @param  array{
     *     withdrawn_on: string,
     *     amount: float|int|string,
     *     received_by_name?: string|null,
     *     notes?: string|null,
     * }  $data
     */
    public static function createWithdrawal(array $data, User $actor): OwnerWithdrawal
    {
        $withdrawnOn = Carbon::parse($data['withdrawn_on'])->toDateString();
        $amount = Money::toFloat($data['amount']);

        if ($amount <= 0) {
            throw ValidationException::withMessages([
                'amount' => __('Withdrawal amount must be greater than zero.'),
            ]);
        }

        $available = self::remainingBalance();

        if ($amount - $available > 0.00001) {
            throw ValidationException::withMessages([
                'amount' => __('Withdrawal cannot exceed available cash with the manager (:amount).', [
                    'amount' => ExpenseReport::formatAmount($available),
                ]),
            ]);
        }

        return OwnerWithdrawal::query()->create([
            'branch_id' => BranchScope::resolveBranchId($actor),
            'withdrawn_on' => $withdrawnOn,
            'amount' => $amount,
            'received_by_name' => $data['received_by_name'] ?? null,
            'notes' => $data['notes'] ?? null,
            'updated_by' => $actor->id,
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    public static function withdrawalPayload(OwnerWithdrawal $withdrawal): array
    {
        $withdrawal->loadMissing(['creator:id,name']);
        $amount = Money::toFloat($withdrawal->amount);

        return [
            'id' => $withdrawal->id,
            'withdrawn_on' => $withdrawal->withdrawn_on?->toDateString(),
            'amount' => $amount,
            'amount_afn' => ExpenseReport::formatAmount($amount),
            'received_by_name' => $withdrawal->received_by_name,
            'notes' => $withdrawal->notes,
            'created_by_name' => $withdrawal->creator?->name,
            'created_at' => $withdrawal->created_at?->toDateTimeString(),
        ];
    }

    public static function remainingBalance(?string $asOf = null): float
    {
        $asOfDate = $asOf !== null && $asOf !== ''
            ? Carbon::parse($asOf)->toDateString()
            : Carbon::now()->toDateString();

        $history = self::buildDailyRows(null, $asOfDate);

        if ($history->isEmpty()) {
            return 0.0;
        }

        $latest = $history->first(
            fn (array $row): bool => $row['date'] <= $asOfDate,
        );

        return Money::toFloat($latest['running_balance'] ?? 0);
    }

    /**
     * @return array{
     *     sales_received: float,
     *     company_expenses: float,
     *     net_cash: float,
     *     owner_withdrawals: float,
     * }
     */
    public static function dayTotals(string $date): array
    {
        $totals = self::totalsByDate()->get($date, self::emptyDayTotals());
        $netCash = Money::toFloat(bcsub(
            Money::round($totals['sales_received']),
            Money::round($totals['company_expenses']),
            2,
        ));

        return [
            'sales_received' => Money::toFloat($totals['sales_received']),
            'company_expenses' => Money::toFloat($totals['company_expenses']),
            'net_cash' => $netCash,
            'owner_withdrawals' => Money::toFloat($totals['owner_withdrawals']),
        ];
    }

    /**
     * @return Collection<int, array<string, mixed>>
     */
    private static function buildDailyRows(?string $dateFrom, ?string $dateTo): Collection
    {
        $totalsByDate = self::totalsByDate();

        if ($totalsByDate->isEmpty()) {
            return collect();
        }

        $allDates = $totalsByDate->keys()->sort()->values();
        $startBound = $allDates->first();
        $endBound = max($allDates->last(), Carbon::now()->toDateString());

        $filterStart = $dateFrom !== null && $dateFrom !== ''
            ? Carbon::parse($dateFrom)->toDateString()
            : $startBound;
        $filterEnd = $dateTo !== null && $dateTo !== ''
            ? Carbon::parse($dateTo)->toDateString()
            : $endBound;

        if ($filterStart > $filterEnd) {
            [$filterStart, $filterEnd] = [$filterEnd, $filterStart];
        }

        $cursor = Carbon::parse($startBound);
        $last = Carbon::parse($endBound);
        $running = '0.00';
        $rows = collect();

        while ($cursor->lte($last)) {
            $date = $cursor->toDateString();
            $totals = $totalsByDate->get($date, self::emptyDayTotals());
            $netCash = Money::toFloat(bcsub(
                Money::round($totals['sales_received']),
                Money::round($totals['company_expenses']),
                2,
            ));
            $withdrawals = Money::toFloat($totals['owner_withdrawals']);

            $running = bcsub(
                bcadd($running, Money::round($netCash), 2),
                Money::round($withdrawals),
                2,
            );

            $remainingWithManager = Money::toFloat(bcsub(
                Money::round($netCash),
                Money::round($withdrawals),
                2,
            ));

            if ($remainingWithManager < 0) {
                $remainingWithManager = 0.0;
            }

            $hasActivity = abs((float) $totals['sales_received']) > 0.00001
                || abs((float) $totals['company_expenses']) > 0.00001
                || abs($withdrawals) > 0.00001;

            if ($hasActivity && $date >= $filterStart && $date <= $filterEnd) {
                $rows->push([
                    'date' => $date,
                    'sales_received' => Money::toFloat($totals['sales_received']),
                    'sales_received_afn' => ExpenseReport::formatAmount(Money::toFloat($totals['sales_received'])),
                    'company_expenses' => Money::toFloat($totals['company_expenses']),
                    'company_expenses_afn' => ExpenseReport::formatAmount(Money::toFloat($totals['company_expenses'])),
                    'net_cash' => $netCash,
                    'net_cash_afn' => ExpenseReport::formatAmount($netCash),
                    'owner_withdrawals' => $withdrawals,
                    'owner_withdrawals_afn' => ExpenseReport::formatAmount($withdrawals),
                    'remaining_with_manager' => $remainingWithManager,
                    'remaining_with_manager_afn' => ExpenseReport::formatAmount($remainingWithManager),
                    'running_balance' => Money::toFloat($running),
                    'running_balance_afn' => ExpenseReport::formatAmount(Money::toFloat($running)),
                ]);
            }

            $cursor->addDay();
        }

        return $rows->sortByDesc('date')->values();
    }

    /**
     * @return Collection<string, array{sales_received: float, company_expenses: float, owner_withdrawals: float}>
     */
    private static function totalsByDate(): Collection
    {
        return (function (): Collection {
            $map = [];

            $ensure = function (string $date) use (&$map): void {
                if (! isset($map[$date])) {
                    $map[$date] = self::emptyDayTotals();
                }
            };

            $payments = SalePayment::query()
                ->whereHas('sale', fn ($query) => $query->where('type', SaleType::SALE))
                ->get(['bill_amount', 'paid_at']);

            foreach ($payments->groupBy(fn (SalePayment $payment): string => $payment->paid_at?->toDateString() ?? '') as $date => $group) {
                if ($date === '') {
                    continue;
                }

                $ensure($date);
                $map[$date]['sales_received'] = Money::toFloat($group->sum(
                    fn (SalePayment $payment): float => (float) $payment->bill_amount,
                ));
            }

            $expensesQuery = Expense::query()->where('status', ExpenseStatus::PAID);
            BranchScope::apply($expensesQuery);

            foreach ($expensesQuery->get(['amount_original', 'expense_date'])->groupBy(
                fn (Expense $expense): string => $expense->expense_date?->toDateString() ?? '',
            ) as $date => $group) {
                if ($date === '') {
                    continue;
                }

                $ensure($date);
                $map[$date]['company_expenses'] = Money::toFloat($group->sum(
                    fn (Expense $expense): float => (float) $expense->amount_original,
                ));
            }

            $withdrawalsQuery = OwnerWithdrawal::query();
            BranchScope::apply($withdrawalsQuery);

            foreach ($withdrawalsQuery->get(['amount', 'withdrawn_on'])->groupBy(
                fn (OwnerWithdrawal $withdrawal): string => $withdrawal->withdrawn_on?->toDateString() ?? '',
            ) as $date => $group) {
                if ($date === '') {
                    continue;
                }

                $ensure($date);
                $map[$date]['owner_withdrawals'] = Money::toFloat($group->sum(
                    fn (OwnerWithdrawal $withdrawal): float => (float) $withdrawal->amount,
                ));
            }

            return collect($map);
        })();
    }

    /**
     * @return array{sales_received: float, company_expenses: float, owner_withdrawals: float}
     */
    private static function emptyDayTotals(): array
    {
        return [
            'sales_received' => 0.0,
            'company_expenses' => 0.0,
            'owner_withdrawals' => 0.0,
        ];
    }
}
