<?php

namespace App\Services\Wallet;

use Carbon\Carbon;
use App\Models\WalletTxn;
use App\Models\TransferOrder;
use App\Models\ReconcileJob;
use App\Models\ReconcileItem;
use App\Support\AdminScopeContext;
use App\Support\AdminScopePolicy;
use Illuminate\Support\Str;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * 钱包划转自动对账扫描器。
 *
 * 作用：
 * - 发现主站与彩票侧的流水不一致
 * - 发现长时间停留在 processing / pending_reconcile 的划转单
 * - 将结果落到 reconcile_jobs / reconcile_items，供后台查看与后续处理
 */
final class WalletTransferReconcileDetector
{
    private const RECONCILE_TYPE = 'wallet_transfer';

    public function scanAndRecord(
        ?Carbon $periodStart = null,
        ?Carbon $periodEnd = null,
        int $limit = 500,
        int $staleMinutes = 15,
        ?int $playerId = null,
        bool $includeMainSiteCheck = true,
    ): ?ReconcileJob {
        $periodEnd ??= now();
        $periodStart ??= $periodEnd->copy()->subDay();

        [$items, $orders] = $this->scanItems($periodStart, $periodEnd, $limit, $staleMinutes, $playerId, $includeMainSiteCheck);

        if ($items === []) {
            return null;
        }

        return $this->persistJob($items, $periodStart, $periodEnd, null);
    }

    /**
     * @return array{
     *   0: list<array{side_a_ref: string, side_b_ref: ?string, difference_amount: int, status: string}>,
     *   1: \Illuminate\Support\Collection<int, TransferOrder>
     * }
     */
    public function scanItems(
        Carbon $periodStart,
        Carbon $periodEnd,
        int $limit = 500,
        int $staleMinutes = 15,
        ?int $playerId = null,
        bool $includeMainSiteCheck = true,
        ?AdminScopeContext $scope = null,
    ): array {
        $limit = max(1, $limit);
        $staleMinutes = max(1, $staleMinutes);
        $staleCutoff = $periodEnd->copy()->subMinutes($staleMinutes);

        $query = TransferOrder::query()
            ->where(function ($q) use ($periodStart, $periodEnd, $staleCutoff): void {
                $q->whereBetween('created_at', [$periodStart, $periodEnd])
                    ->orWhere('status', 'pending_reconcile')
                    ->orWhere(function ($inner) use ($staleCutoff): void {
                        $inner->where('status', 'processing')
                            ->where('created_at', '<=', $staleCutoff);
                    });
            })
            ->orderBy('id')
            ->limit($limit);

        if ($playerId !== null) {
            $query->where('player_id', $playerId);
        }

        if ($scope !== null && ! $scope->isSuperAdmin()) {
            AdminScopePolicy::applyViaPlayer($query, $scope);
        }

        $orders = $query->get();

        if ($orders->isEmpty()) {
            return [[], $orders];
        }

        $txnsByTransferNo = WalletTxn::query()
            ->whereIn('biz_no', $orders->pluck('transfer_no')->all())
            ->orderBy('id')
            ->get()
            ->groupBy(static fn (WalletTxn $txn): string => (string) $txn->biz_no);

        $items = [];
        foreach ($orders as $order) {
            $issue = $this->issueForOrder(
                $order,
                $txnsByTransferNo->get((string) $order->transfer_no, collect()),
                $periodEnd,
                $staleMinutes,
            );

            if ($issue !== null) {
                $items[] = $issue;
            }
        }

        if ($includeMainSiteCheck) {
            $items = app(WalletTransferMainSiteReconcileChecker::class)->appendIssues($items, $orders);
        }

        return [$items, $orders];
    }

    /**
     * @param  list<array{side_a_ref: string, side_b_ref: ?string, difference_amount: int, status: string}>  $items
     */
    public function persistJob(
        array $items,
        Carbon $periodStart,
        Carbon $periodEnd,
        ?int $adminUserId,
        ?int $adminSiteId = null,
        ?int $playerId = null,
    ): ReconcileJob {
        return DB::transaction(function () use ($items, $periodStart, $periodEnd, $adminUserId, $adminSiteId, $playerId): ReconcileJob {
            $jobNo = 'REC'.now()->format('YmdHis').strtoupper(str_replace('-', '', Str::uuid()->toString()));

            $job = ReconcileJob::query()->create([
                'job_no' => $jobNo,
                'admin_user_id' => $adminUserId,
                'admin_site_id' => $adminSiteId,
                'reconcile_type' => self::RECONCILE_TYPE,
                'status' => 'completed',
                'period_start' => $periodStart,
                'period_end' => $periodEnd,
                'summary_json' => [
                    'item_count' => count($items),
                    'mismatch_count' => count($items),
                    'player_id' => $playerId,
                ],
                'finished_at' => now(),
            ]);

            foreach ($items as $row) {
                ReconcileItem::query()->create([
                    'reconcile_job_id' => (int) $job->getKey(),
                    'side_a_ref' => $row['side_a_ref'],
                    'side_b_ref' => $row['side_b_ref'],
                    'difference_amount' => (int) $row['difference_amount'],
                    'status' => $row['status'],
                    'resolved_at' => null,
                ]);
            }

            return $job->fresh(['items']);
        });
    }

    /**
     * @return array{side_a_ref: string, side_b_ref: ?string, difference_amount: int, status: string}|null
     */
    private function issueForOrder(TransferOrder $order, Collection $txns, Carbon $periodEnd, int $staleMinutes): ?array
    {
        $amount = (int) $order->amount;
        $latestTxnNo = $this->latestTxnNo($txns);
        $createdAt = $order->created_at;

        if ($order->status === 'processing') {
            if ($createdAt !== null && $createdAt->greaterThan($periodEnd->copy()->subMinutes($staleMinutes))) {
                return null;
            }

            return [
                'side_a_ref' => (string) $order->transfer_no,
                'side_b_ref' => $latestTxnNo,
                'difference_amount' => $amount,
                'status' => 'stale_processing',
            ];
        }

        if ($order->status === 'pending_reconcile') {
            return [
                'side_a_ref' => (string) $order->transfer_no,
                'side_b_ref' => $latestTxnNo,
                'difference_amount' => $amount,
                'status' => 'pending_reconcile',
            ];
        }

        if ($order->status === 'success') {
            $primaryBizType = $order->direction === 'out' ? 'transfer_out' : 'transfer_in';
            if (! $this->hasTxn($txns, $primaryBizType, 'posted')) {
                return [
                    'side_a_ref' => (string) $order->transfer_no,
                    'side_b_ref' => $latestTxnNo,
                    'difference_amount' => $amount,
                    'status' => 'missing_wallet_txn',
                ];
            }

            if ($order->direction === 'out' && $this->hasTxn($txns, 'transfer_out_refund')) {
                return [
                    'side_a_ref' => (string) $order->transfer_no,
                    'side_b_ref' => $latestTxnNo,
                    'difference_amount' => $amount,
                    'status' => 'unexpected_wallet_txn',
                ];
            }

            if ($order->direction === 'in' && $txns->count() !== 1) {
                return [
                    'side_a_ref' => (string) $order->transfer_no,
                    'side_b_ref' => $latestTxnNo,
                    'difference_amount' => $amount,
                    'status' => 'unexpected_wallet_txn',
                ];
            }

            return null;
        }

        if ($order->status === 'failed') {
            if ($order->direction === 'in') {
                if ($txns->isNotEmpty()) {
                    return [
                        'side_a_ref' => (string) $order->transfer_no,
                        'side_b_ref' => $latestTxnNo,
                        'difference_amount' => $amount,
                        'status' => 'unexpected_wallet_txn',
                    ];
                }

                return null;
            }

            if (($order->fail_reason ?? '') === 'insufficient_balance') {
                if ($txns->isNotEmpty()) {
                    return [
                        'side_a_ref' => (string) $order->transfer_no,
                        'side_b_ref' => $latestTxnNo,
                        'difference_amount' => $amount,
                        'status' => 'unexpected_wallet_txn',
                    ];
                }

                return null;
            }

            $hasTransferOut = $this->hasTxn($txns, 'transfer_out', 'posted');
            $hasRefund = $this->hasTxn($txns, 'transfer_out_refund', 'posted');
            if (! $hasTransferOut || ! $hasRefund) {
                return [
                    'side_a_ref' => (string) $order->transfer_no,
                    'side_b_ref' => $latestTxnNo,
                    'difference_amount' => $amount,
                    'status' => ! $hasTransferOut ? 'missing_wallet_txn' : 'missing_refund',
                ];
            }

            return null;
        }

        if ($order->status === 'reversed') {
            if ($order->direction === 'out') {
                $hasTransferOut = $this->hasTxn($txns, 'transfer_out', 'posted');
                $hasReversal = $this->hasTxn($txns, 'reversal', 'posted');
                if (! $hasTransferOut || ! $hasReversal) {
                    return [
                        'side_a_ref' => (string) $order->transfer_no,
                        'side_b_ref' => $latestTxnNo,
                        'difference_amount' => $amount,
                        'status' => ! $hasReversal ? 'missing_reversal' : 'missing_wallet_txn',
                    ];
                }
            }

            return null;
        }

        return null;
    }

    private function hasTxn(Collection $txns, string $bizType, ?string $status = null): bool
    {
        return $txns->contains(static function (WalletTxn $txn) use ($bizType, $status): bool {
            if ($txn->biz_type !== $bizType) {
                return false;
            }

            return $status === null || $txn->status === $status;
        });
    }

    private function latestTxnNo(Collection $txns): ?string
    {
        /** @var WalletTxn|null $txn */
        $txn = $txns->sortByDesc('id')->first();

        return $txn !== null ? (string) $txn->txn_no : null;
    }
}
