<?php

namespace App\Services;

use App\Models\DayLedger;
use App\Models\LedgerEntry;
use App\Models\Order;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class LedgerService
{
    /**
     * Ledger payment modes exposed to admin users.
     */
    public const PAYMENT_MODES = [
        'cash',
        'card_upi',
        'offline',
    ];

    public const PAYMENT_MODE_LABELS = [
        'cash' => 'Cash',
        'card_upi' => 'Card/UPI',
        'offline' => 'Offline',
    ];

    public const ORDER_TYPE_ALL = 'all';
    public const ORDER_TYPE_POS = 'pos';
    public const ORDER_TYPE_ONLINE = 'online';

    /**
     * Build complete overview payload for Day Ledger screen.
     */
    public function getOverview(string $date, ?string $paymentMode = null, string $orderType = self::ORDER_TYPE_ALL): array
    {
        $ledgerDate = Carbon::parse($date)->toDateString();
        $dayLedger = DayLedger::query()->whereDate('date', $ledgerDate)->first();
        $previousDay = DayLedger::query()
            ->whereDate('date', '<', $ledgerDate)
            ->orderByDesc('date')
            ->first();

        $openingBalance = $dayLedger
            ? (float) $dayLedger->opening_balance
            : $this->resolveOpeningBalance($ledgerDate, null);

        $orderAgg = $this->aggregateOrders($ledgerDate, $paymentMode, $orderType);
        $manualAgg = $this->aggregateManualEntries($ledgerDate, $paymentMode);

        $totalCollection = (float) $orderAgg['total_collection']
            + (float) $manualAgg['total_inflow'];
        $totalExpense = (float) $manualAgg['total_expense'];
        $totalRefund = (float) $orderAgg['total_refund'] + (float) $manualAgg['total_refund'];

        // Opening/closing represents cash in counter only.
        $cashCollectionForClosing = (float) $orderAgg['cash_collection'] + (float) $manualAgg['cash_inflow'];
        $cashRefundForClosing = (float) $orderAgg['cash_refund'] + (float) $manualAgg['cash_refund'];
        $cashExpenseForClosing = (float) $manualAgg['cash_expense'];
        $cashToBankTransfer = (float) $manualAgg['cash_to_bank_transfer'];
        $closingBalance = round(
            $openingBalance
            + $cashCollectionForClosing
            - $cashExpenseForClosing
            - $cashRefundForClosing
            - $cashToBankTransfer,
            2
        );

        return [
            'date' => $ledgerDate,
            'status' => $dayLedger?->status ?? DayLedger::STATUS_OPEN,
            'day_ledger' => $dayLedger,
            'requires_opening_balance_input' => !$dayLedger && !$previousDay,
            'opening_balance' => round($openingBalance, 2),
            'total_sales' => round((float) $orderAgg['total_sales'], 2),
            'total_collection' => round($totalCollection, 2),
            'total_expense' => round($totalExpense, 2),
            'total_refund' => round($totalRefund, 2),
            'closing_balance' => $closingBalance,
            'cash_to_bank_transfer' => round($cashToBankTransfer, 2),
            'payment_breakdown' => $this->buildPaymentModeBreakdown($orderAgg['collection_by_mode'], $manualAgg['manual_by_mode']),
            'transactions' => $this->buildDetailedTransactions($ledgerDate, $paymentMode, $orderType),
            'filters' => [
                'payment_mode' => $paymentMode,
                'order_type' => $orderType,
            ],
        ];
    }

    public function resolveOpeningBalance(string $date, ?float $manualOpening = null): float
    {
        $ledgerDate = Carbon::parse($date)->toDateString();

        $previousDay = DayLedger::query()
            ->whereDate('date', '<', $ledgerDate)
            ->orderByDesc('date')
            ->first();

        if ($previousDay) {
            return (float) $previousDay->closing_balance;
        }

        return max(0, (float) ($manualOpening ?? 0));
    }

    /**
     * Ensure ledger row exists for date and return locked/open record.
     */
    public function ensureDayLedger(string $date, ?float $manualOpening = null): DayLedger
    {
        $ledgerDate = Carbon::parse($date)->toDateString();

        $existing = DayLedger::query()->whereDate('date', $ledgerDate)->first();
        if ($existing) {
            return $existing;
        }

        $opening = $this->resolveOpeningBalance($ledgerDate, $manualOpening);

        return DayLedger::query()->create([
            'date' => $ledgerDate,
            'opening_balance' => $opening,
            'total_sales' => 0,
            'total_collection' => 0,
            'total_expense' => 0,
            'total_refund' => 0,
            'closing_balance' => $opening,
            'status' => DayLedger::STATUS_OPEN,
        ]);
    }

    /**
     * Close a day and persist immutable totals snapshot.
     */
    public function closeDay(string $date, int $closedBy, ?float $manualOpening = null): DayLedger
    {
        return DB::transaction(function () use ($date, $closedBy, $manualOpening) {
            $ledgerDate = Carbon::parse($date)->toDateString();
            $ledger = DayLedger::query()->whereDate('date', $ledgerDate)->lockForUpdate()->first();

            if (!$ledger) {
                $this->ensureDayLedger($ledgerDate, $manualOpening);
                $ledger = DayLedger::query()->whereDate('date', $ledgerDate)->lockForUpdate()->first();
            }

            if ($ledger->status === DayLedger::STATUS_CLOSED) {
                throw ValidationException::withMessages([
                    'date' => 'This day ledger is already closed.',
                ]);
            }

            $overview = $this->getOverview($ledgerDate, null, self::ORDER_TYPE_ALL);

            $ledger->update([
                'opening_balance' => $overview['opening_balance'],
                'total_sales' => $overview['total_sales'],
                'total_collection' => $overview['total_collection'],
                'total_expense' => $overview['total_expense'],
                'total_refund' => $overview['total_refund'],
                'closing_balance' => $overview['closing_balance'],
                'status' => DayLedger::STATUS_CLOSED,
                'closed_by' => $closedBy,
                'closed_at' => now(),
            ]);

            return $ledger->fresh();
        });
    }

    public function reopenDay(string $date, bool $isSuperAdmin): DayLedger
    {
        if (!$isSuperAdmin) {
            throw ValidationException::withMessages([
                'permission' => 'Only super admin can reopen a closed day.',
            ]);
        }

        return DB::transaction(function () use ($date) {
            $ledgerDate = Carbon::parse($date)->toDateString();
            $ledger = DayLedger::query()->whereDate('date', $ledgerDate)->lockForUpdate()->first();

            if (!$ledger) {
                throw ValidationException::withMessages([
                    'date' => 'Day ledger does not exist for the selected date.',
                ]);
            }

            $ledger->update([
                'status' => DayLedger::STATUS_OPEN,
                'closed_by' => null,
                'closed_at' => null,
            ]);

            return $ledger->fresh();
        });
    }

    /**
     * Guard for actions that must not happen on closed day.
     */
    public function assertDayOpen(string $date): void
    {
        $ledgerDate = Carbon::parse($date)->toDateString();
        $closed = DayLedger::query()
            ->whereDate('date', $ledgerDate)
            ->where('status', DayLedger::STATUS_CLOSED)
            ->exists();

        if ($closed) {
            throw ValidationException::withMessages([
                'date' => 'This day is closed. Reopen the day to modify records.',
            ]);
        }
    }

    public function createManualEntry(array $data): LedgerEntry
    {
        $ledgerDate = Carbon::parse($data['ledger_date'])->toDateString();
        $this->assertDayOpen($ledgerDate);
        $this->ensureDayLedger($ledgerDate, null);

        return LedgerEntry::query()->create([
            'ledger_date' => $ledgerDate,
            'entry_type' => $data['entry_type'],
            'reference_type' => $data['reference_type'] ?? LedgerEntry::REFERENCE_MANUAL,
            'reference_id' => $data['reference_id'] ?? null,
            'payment_mode' => $data['payment_mode'],
            'amount' => $data['amount'],
            'remarks' => $data['remarks'] ?? null,
            'created_by' => $data['created_by'] ?? Auth::id(),
        ]);
    }

    public function aggregateMonthly(string $month): Collection
    {
        $start = Carbon::parse($month . '-01')->startOfMonth()->toDateString();
        $end = Carbon::parse($month . '-01')->endOfMonth()->toDateString();

        return DayLedger::query()
            ->whereBetween('date', [$start, $end])
            ->orderBy('date')
            ->get([
                'date',
                'opening_balance',
                'total_sales',
                'total_collection',
                'total_expense',
                'total_refund',
                'closing_balance',
                'status',
            ]);
    }

    public function aggregatePaymentModes(string $date, string $orderType = self::ORDER_TYPE_ALL): array
    {
        $orderAgg = $this->aggregateOrders($date, null, $orderType);
        $manualAgg = $this->aggregateManualEntries($date, null);

        return $this->buildPaymentModeBreakdown($orderAgg['collection_by_mode'], $manualAgg['manual_by_mode']);
    }

    private function aggregateOrders(string $date, ?string $paymentMode, string $orderType): array
    {
        $paidOrders = $this->orderQueryByFilters($date, $paymentMode, $orderType)
            ->where('payment_status', Order::PAYMENT_PAID)
            ->whereNotIn('status', [Order::STATUS_CANCELLED, Order::STATUS_RETURNED]);

        $refundedOrders = $this->orderQueryByFilters($date, $paymentMode, $orderType)
            ->where('payment_status', Order::PAYMENT_REFUNDED);

        $paidOrderRows = (clone $paidOrders)
            ->select(['payment_method', 'total_payable', 'cash_amount', 'card_amount', 'change_amount'])
            ->get();

        $collectionByMode = [];
        foreach ($paidOrderRows as $row) {
            $alloc = $this->allocateCollectionByMode(
                (string) $row->payment_method,
                (float) $row->total_payable,
                $row->cash_amount !== null ? (float) $row->cash_amount : null,
                $row->card_amount !== null ? (float) $row->card_amount : null,
                $row->change_amount !== null ? (float) $row->change_amount : null
            );
            foreach ($alloc as $mode => $amount) {
                $collectionByMode[$mode] = (float) ($collectionByMode[$mode] ?? 0) + (float) $amount;
            }
        }

        $refundedOrderRows = (clone $refundedOrders)
            ->select(['payment_method', 'total_payable', 'cash_amount', 'card_amount', 'change_amount'])
            ->get();
        $cashRefund = 0.0;
        foreach ($refundedOrderRows as $row) {
            $alloc = $this->allocateCollectionByMode(
                (string) $row->payment_method,
                (float) $row->total_payable,
                $row->cash_amount !== null ? (float) $row->cash_amount : null,
                $row->card_amount !== null ? (float) $row->card_amount : null,
                $row->change_amount !== null ? (float) $row->change_amount : null
            );
            $cashRefund += (float) ($alloc['cash'] ?? 0);
        }

        return [
            'total_sales' => (float) (clone $paidOrders)->sum('total_payable'),
            'total_collection' => (float) (clone $paidOrders)->sum('total_payable'),
            'total_refund' => (float) (clone $refundedOrders)->sum('total_payable'),
            'cash_collection' => (float) ($collectionByMode['cash'] ?? 0),
            'cash_refund' => round($cashRefund, 2),
            'collection_by_mode' => $collectionByMode,
        ];
    }

    private function aggregateManualEntries(string $date, ?string $paymentMode): array
    {
        $query = LedgerEntry::query()->whereDate('ledger_date', Carbon::parse($date)->toDateString());

        if ($paymentMode) {
            $query->where('payment_mode', $paymentMode);
        }

        $rows = (clone $query)
            ->select('entry_type', 'payment_mode', DB::raw('SUM(amount) as amount'))
            ->groupBy('entry_type', 'payment_mode')
            ->get();

        $manualByMode = [];
        $totalExpense = 0.0;
        $totalRefund = 0.0;
        $totalInflow = 0.0;
        $cashInflow = 0.0;
        $cashExpense = 0.0;
        $cashRefund = 0.0;
        $cashToBankTransfer = 0.0;

        foreach ($rows as $row) {
            $mode = $row->payment_mode;
            $type = $row->entry_type;
            $amount = (float) $row->amount;

            $manualByMode[$mode] ??= [
                'collection' => 0.0,
                'expense' => 0.0,
                'refund' => 0.0,
                'transfer' => 0.0,
            ];

            if (in_array($type, [LedgerEntry::TYPE_ADVANCE, LedgerEntry::TYPE_ADJUSTMENT], true)) {
                $manualByMode[$mode]['collection'] += $amount;
                $totalInflow += $amount;
                if ($mode === 'cash') {
                    $cashInflow += $amount;
                }
                continue;
            }

            if ($type === LedgerEntry::TYPE_EXPENSE) {
                $manualByMode[$mode]['expense'] += $amount;
                $totalExpense += $amount;
                if ($mode === 'cash') {
                    $cashExpense += $amount;
                }
                continue;
            }

            if ($type === LedgerEntry::TYPE_REFUND) {
                $manualByMode[$mode]['refund'] += $amount;
                $totalRefund += $amount;
                if ($mode === 'cash') {
                    $cashRefund += $amount;
                }
                continue;
            }

            if ($type === LedgerEntry::TYPE_CASH_TO_BANK) {
                $manualByMode[$mode]['transfer'] += $amount;
                $cashToBankTransfer += $amount;
            }
        }

        return [
            'total_inflow' => round($totalInflow, 2),
            'total_expense' => round($totalExpense, 2),
            'total_refund' => round($totalRefund, 2),
            'cash_inflow' => round($cashInflow, 2),
            'cash_expense' => round($cashExpense, 2),
            'cash_refund' => round($cashRefund, 2),
            'cash_to_bank_transfer' => round($cashToBankTransfer, 2),
            'manual_by_mode' => $manualByMode,
        ];
    }

    private function orderQueryByFilters(string $date, ?string $paymentMode, string $orderType): Builder
    {
        $query = Order::query()->whereDate('created_at', Carbon::parse($date)->toDateString());

        if ($paymentMode) {
            if ($paymentMode === 'cash') {
                $query->whereIn('payment_method', ['cash', 'cod']);
            } elseif ($paymentMode === 'card_upi') {
                $query->whereIn('payment_method', ['card', 'online', 'digital_wallet']);
            } elseif ($paymentMode === 'offline') {
                $query->where('payment_method', 'offline');
            }
        }

        if ($orderType === self::ORDER_TYPE_ONLINE) {
            $query->where('source', 'online');
        } elseif ($orderType === self::ORDER_TYPE_POS) {
            $query->whereIn('source', ['pos', 'pos_to_online']);
        }

        return $query;
    }

    private function buildDetailedTransactions(string $date, ?string $paymentMode, string $orderType): array
    {
        $ordersQuery = $this->orderQueryByFilters($date, $paymentMode, $orderType)
            ->with(['user:id,name,mobile', 'staff:id,name'])
            ->whereIn('payment_status', [Order::PAYMENT_PAID, Order::PAYMENT_REFUNDED])
            ->orderBy('created_at', 'desc');

        $manualQuery = LedgerEntry::query()
            ->with('creator:id,name')
            ->whereDate('ledger_date', Carbon::parse($date)->toDateString())
            ->orderBy('created_at', 'desc');

        if ($paymentMode) {
            $manualQuery->where('payment_mode', $paymentMode);
        }

        $orderRows = $ordersQuery->get()->map(function (Order $order): array {
            return [
                'kind' => 'order',
                'datetime' => $order->created_at->toDateTimeString(),
                'id' => $order->id,
                'reference' => '#' . $order->id,
                'payment_mode' => $this->normalizeOrderPaymentMode((string) $order->payment_method),
                'entry_type' => $order->payment_status === Order::PAYMENT_REFUNDED ? 'refund' : 'collection',
                'amount' => (float) $order->total_payable,
                'order_type' => $order->source,
                'notes' => $order->payment_status === Order::PAYMENT_REFUNDED ? 'Order refund' : 'Order collection',
                'customer' => $order->user?->name ?? 'Guest',
                'staff' => $order->staff?->name,
            ];
        });

        $manualRows = $manualQuery->get()->map(function (LedgerEntry $entry): array {
            return [
                'kind' => 'manual',
                'datetime' => $entry->created_at->toDateTimeString(),
                'id' => $entry->id,
                'reference' => strtoupper($entry->entry_type) . '-' . $entry->id,
                'payment_mode' => $entry->payment_mode,
                'entry_type' => $entry->entry_type,
                'amount' => (float) $entry->amount,
                'order_type' => '-',
                'notes' => $entry->remarks,
                'customer' => '-',
                'staff' => $entry->creator?->name,
            ];
        });

        return $orderRows
            ->concat($manualRows)
            ->sortByDesc('datetime')
            ->values()
            ->all();
    }

    private function buildPaymentModeBreakdown(array $collectionByMode, array $manualByMode): array
    {
        $modes = array_values(array_unique(array_merge(
            array_keys($collectionByMode),
            array_keys($manualByMode),
            self::PAYMENT_MODES
        )));

        $rows = [];
        foreach ($modes as $mode) {
            $orderCollection = (float) ($collectionByMode[$mode] ?? 0);
            $manual = $manualByMode[$mode] ?? ['collection' => 0.0, 'expense' => 0.0, 'refund' => 0.0, 'transfer' => 0.0];
            $collection = $orderCollection + (float) $manual['collection'];
            $expense = (float) $manual['expense'];
            $refund = (float) $manual['refund'];
            $transfer = (float) $manual['transfer'];

            $rows[] = [
                'payment_mode' => $mode,
                'collection' => round($collection, 2),
                'expense' => round($expense, 2),
                'refund' => round($refund, 2),
                'transfer' => round($transfer, 2),
                'net' => round($collection - $expense - $refund - $transfer, 2),
            ];
        }

        return $rows;
    }

    private function normalizeOrderPaymentMode(string $paymentMethod): string
    {
        return match ($paymentMethod) {
            'cash', 'cod' => 'cash',
            'card', 'online', 'digital_wallet' => 'card_upi',
            default => 'offline',
        };
    }

    /**
     * Split one order collection into ledger modes.
     * Handles POS split cash/card entries and normal single-mode payments.
     */
    private function allocateCollectionByMode(
        string $paymentMethod,
        float $totalPayable,
        ?float $cashAmount,
        ?float $cardAmount,
        ?float $changeAmount
    ): array {
        $total = max(0, (float) $totalPayable);
        $cash = max(0, (float) ($cashAmount ?? 0));
        $card = max(0, (float) ($cardAmount ?? 0));
        $change = max(0, (float) ($changeAmount ?? 0));

        // Adjust cash by returned change first; card should remain as collected.
        if ($cash > 0 || $card > 0) {
            $cash = max(0, $cash - $change);
            $allocated = $cash + $card;

            if ($allocated > 0) {
                // Normalize to order payable so split sum never exceeds/undershoots booked collection.
                if ($allocated > $total) {
                    $overflow = $allocated - $total;
                    if ($cash >= $overflow) {
                        $cash -= $overflow;
                    } else {
                        $overflow -= $cash;
                        $cash = 0;
                        $card = max(0, $card - $overflow);
                    }
                } elseif ($allocated < $total) {
                    $remainder = $total - $allocated;
                    $fallbackMode = $this->normalizeOrderPaymentMode($paymentMethod);
                    if ($fallbackMode === 'cash') {
                        $cash += $remainder;
                    } elseif ($fallbackMode === 'card_upi') {
                        $card += $remainder;
                    } else {
                        return [
                            'cash' => round($cash, 2),
                            'card_upi' => round($card, 2),
                            'offline' => round($remainder, 2),
                        ];
                    }
                }

                return [
                    'cash' => round($cash, 2),
                    'card_upi' => round($card, 2),
                    'offline' => 0.0,
                ];
            }
        }

        $mode = $this->normalizeOrderPaymentMode($paymentMethod);
        return [
            'cash' => $mode === 'cash' ? round($total, 2) : 0.0,
            'card_upi' => $mode === 'card_upi' ? round($total, 2) : 0.0,
            'offline' => $mode === 'offline' ? round($total, 2) : 0.0,
        ];
    }
}
