<?php

namespace App\Services\Settlement;

use App\Models\Draw;
use App\Models\Player;
use App\Models\AdminUser;
use App\Models\TicketItem;
use App\Lottery\DrawStatus;
use App\Models\JackpotPool;
use App\Models\TicketOrder;
use App\Models\SettlementBatch;
use Illuminate\Support\Facades\DB;
use App\Lottery\SettlementBatchStatus;
use App\Services\AgentSettlement\GameSettlementReversalService;
use App\Services\Ticket\TicketWalletService;
use App\Support\PlayerFundingMode;

final class SettlementBatchWorkflowService
{
    public function __construct(
        private readonly TicketWalletService $wallet,
        private readonly GameSettlementReversalService $gameSettlementReversal,
    ) {}

    public function approve(SettlementBatch $batch, AdminUser $admin, ?string $remark = null): SettlementBatch
    {
        return $this->approveInternal($batch, $admin->id, $remark);
    }

    public function approveBySystem(SettlementBatch $batch, ?string $remark = null): SettlementBatch
    {
        return $this->approveInternal($batch, null, $remark);
    }

    private function approveInternal(SettlementBatch $batch, ?int $reviewedBy, ?string $remark): SettlementBatch
    {
        return DB::transaction(function () use ($batch, $reviewedBy, $remark): SettlementBatch {
            /** @var SettlementBatch $locked */
            $locked = SettlementBatch::query()->whereKey($batch->id)->lockForUpdate()->firstOrFail();
            if ($locked->status !== SettlementBatchStatus::PendingReview->value) {
                throw new \RuntimeException('settlement_not_pending_review');
            }

            $locked->forceFill([
                'status' => SettlementBatchStatus::Approved->value,
                'review_status' => 'approved',
                'reviewed_by' => $reviewedBy,
                'reviewed_at' => now(),
                'review_remark' => $remark,
            ])->save();

            return $locked->refresh();
        });
    }

    public function reject(SettlementBatch $batch, AdminUser $admin, ?string $remark = null): SettlementBatch
    {
        return DB::transaction(function () use ($batch, $admin, $remark): SettlementBatch {
            /** @var SettlementBatch $locked */
            $locked = SettlementBatch::query()->whereKey($batch->id)->lockForUpdate()->firstOrFail();
            if ($locked->status !== SettlementBatchStatus::PendingReview->value) {
                throw new \RuntimeException('settlement_not_pending_review');
            }

            $itemIds = $locked->details()->pluck('ticket_item_id')->map(fn ($id) => (int) $id)->all();
            if ($itemIds !== []) {
                $items = TicketItem::query()->whereIn('id', $itemIds)->get();
                foreach ($items as $item) {
                    $this->gameSettlementReversal->reverseTicketItem($item);
                }

                TicketItem::query()
                    ->whereIn('id', $itemIds)
                    ->update([
                        'status' => 'pending_draw',
                        'win_amount' => 0,
                        'jackpot_win_amount' => 0,
                        'settled_at' => null,
                        'agent_settled_at' => null,
                    ]);
            }

            $this->restoreJackpotPoolAfterReject($locked);

            $locked->forceFill([
                'status' => SettlementBatchStatus::Rejected->value,
                'review_status' => 'rejected',
                'reviewed_by' => $admin->id,
                'reviewed_at' => now(),
                'review_remark' => $remark,
            ])->save();

            return $locked->refresh();
        });
    }

    public function payout(SettlementBatch $batch): SettlementBatch
    {
        return DB::transaction(function () use ($batch): SettlementBatch {
            /** @var SettlementBatch $locked */
            $locked = SettlementBatch::query()->whereKey($batch->id)->lockForUpdate()->firstOrFail();
            if ($locked->status !== SettlementBatchStatus::Approved->value || $locked->review_status !== 'approved') {
                throw new \RuntimeException('settlement_not_approved');
            }

            $batchItemIds = $locked->details()->pluck('ticket_item_id')->map(fn ($id) => (int) $id)->all();

            $hasUnsettled = TicketItem::query()
                ->where('draw_id', $locked->draw_id)
                ->whereIn('status', [
                    'pending_confirm',
                    'partial_pending_confirm',
                    'pending_draw',
                ])
                ->exists();
            if ($hasUnsettled) {
                throw new \RuntimeException('draw_has_unsettled_tickets');
            }

            if ($batchItemIds !== []) {
                $orphanPendingPayout = TicketItem::query()
                    ->where('draw_id', $locked->draw_id)
                    ->where('status', 'pending_payout')
                    ->whereNotIn('id', $batchItemIds)
                    ->exists();
                if ($orphanPendingPayout) {
                    throw new \RuntimeException('draw_has_unsettled_tickets');
                }
            }

            $details = $locked->details()->with(['ticketItem.order'])->get();
            $playerCurrencyTotals = [];

            foreach ($details as $detail) {
                $item = $detail->ticketItem;
                if ($item === null) {
                    continue;
                }
                $finalCredit = (int) $detail->win_amount + (int) $detail->jackpot_allocation_amount;
                if ($finalCredit > 0) {
                    $pid = (int) $item->player_id;
                    $currency = strtoupper((string) ($item->order?->currency_code ?? 'NPR'));
                    $aggregateKey = $pid.':'.$currency;
                    if (! isset($playerCurrencyTotals[$aggregateKey])) {
                        $playerCurrencyTotals[$aggregateKey] = [
                            'player_id' => $pid,
                            'currency_code' => $currency,
                            'amount' => 0,
                        ];
                    }
                    $playerCurrencyTotals[$aggregateKey]['amount'] += $finalCredit;
                    $item->forceFill(['status' => 'settled_win', 'settled_at' => now()])->save();
                } elseif ($item->status !== 'settled_lose') {
                    $item->forceFill(['status' => 'settled_lose', 'settled_at' => now()])->save();
                }
            }

            foreach ($playerCurrencyTotals as $entry) {
                $amount = (int) $entry['amount'];
                if ($amount <= 0) {
                    continue;
                }
                $player = Player::query()->whereKey((int) $entry['player_id'])->firstOrFail();
                if (PlayerFundingMode::usesCredit($player)) {
                    continue;
                }
                $this->wallet->creditSettlementPayout(
                    $player,
                    (string) $entry['currency_code'],
                    $amount,
                    (int) $locked->id
                );
            }

            $orderIds = TicketItem::query()
                ->whereIn('id', $locked->details()->pluck('ticket_item_id'))
                ->pluck('order_id')
                ->unique()
                ->all();
            foreach ($orderIds as $orderId) {
                $pending = TicketItem::query()
                    ->where('order_id', $orderId)
                    ->whereNotIn('status', ['settled_win', 'settled_lose'])
                    ->exists();
                if (! $pending) {
                    TicketOrder::query()->whereKey($orderId)->update(['status' => 'settled']);
                }
            }

            $locked->forceFill([
                'status' => SettlementBatchStatus::Paid->value,
                'paid_at' => now(),
            ])->save();

            Draw::query()->whereKey($locked->draw_id)->update([
                'status' => DrawStatus::Settled->value,
                'settle_version' => (int) $locked->settle_version,
            ]);

            return $locked->refresh();
        });
    }

    private function restoreJackpotPoolAfterReject(SettlementBatch $batch): void
    {
        $restoreAmount = (int) $batch->total_jackpot_payout_amount;
        if ($restoreAmount <= 0) {
            return;
        }

        $details = $batch->details()->with(['ticketItem.order'])->get();
        $restoreByCurrency = [];
        foreach ($details as $detail) {
            $amount = (int) $detail->jackpot_allocation_amount;
            if ($amount <= 0) {
                continue;
            }
            $currency = strtoupper((string) ($detail->ticketItem?->order?->currency_code ?? 'NPR'));
            $restoreByCurrency[$currency] = ($restoreByCurrency[$currency] ?? 0) + $amount;
        }

        if ($restoreByCurrency === []) {
            return;
        }

        foreach ($restoreByCurrency as $currency => $amount) {
            $pool = JackpotPool::query()
                ->where('currency_code', $currency)
                ->where('status', 1)
                ->lockForUpdate()
                ->first();

            if ($pool === null) {
                continue;
            }

            $pool->forceFill([
                'current_amount' => (int) $pool->current_amount + (int) $amount,
            ])->save();
        }
    }
}
