<?php

namespace App\Services\AgentSettlement;

use Carbon\Carbon;

/**
 * 信用盘下注流水对外展示：待开奖「下注冻结」+ 已结算「开奖结算」（每注单一条，不重复展示占用）。
 */
final class CreditLedgerBetFlowPresenter
{
    public const DISPLAY_BET_HOLD = 'bet_hold';

    public const DISPLAY_GAME_SETTLEMENT = 'game_settlement';

    private const SETTLEMENT_REASONS = ['bet_hold_release', 'game_settlement_loss', 'game_settlement_win'];

    /**
     * @param  list<object>  $rows  credit_ledger 行（含 reason、ref_type、ref_id、amount、created_at）
     * @param  array<int, array{play_code: string|null, draw_no: string|null, ticket_item_id: int, actual_deduct_amount?: int}>  $ticketRefs
     * @return list<array<string, mixed>>
     */
    public function simplifyCreditRows(
        array $rows,
        array $ticketRefs,
        callable $formatHold,
        callable $formatSettlement,
    ): array {
        /** @var list<object> $holdRows */
        $holdRows = [];
        /** @var array<int, list<object>> $byTicket */
        $byTicket = [];

        foreach ($rows as $row) {
            $reason = (string) ($row->reason ?? '');
            if ($reason === self::DISPLAY_BET_HOLD) {
                $holdRows[] = $row;

                continue;
            }

            if (! in_array($reason, self::SETTLEMENT_REASONS, true)) {
                continue;
            }

            $ticketId = $this->ticketItemId($row);
            if ($ticketId <= 0) {
                continue;
            }

            $byTicket[$ticketId][] = $row;
        }

        /** @var list<object> $mergedSettlements */
        $mergedSettlements = [];
        foreach ($byTicket as $ticketId => $entries) {
            $merged = $this->mergeSettlementEntries($ticketId, $entries, $ticketRefs);
            if ($merged !== null) {
                $mergedSettlements[] = $merged;
            }
        }

        $visibleHolds = $this->holdsWithoutSettledMatch($holdRows, $mergedSettlements, $ticketRefs);

        $holdItems = array_map($formatHold, $visibleHolds);
        $settlementItems = array_map(
            fn (object $merged): array => $formatSettlement($merged, $ticketRefs),
            $mergedSettlements,
        );

        $all = array_merge($holdItems, $settlementItems);
        usort($all, static function (array $a, array $b): int {
            $ta = isset($a['created_at']) ? strtotime((string) $a['created_at']) : 0;
            $tb = isset($b['created_at']) ? strtotime((string) $b['created_at']) : 0;
            if ($ta === $tb) {
                return (int) ($b['id'] ?? 0) <=> (int) ($a['id'] ?? 0);
            }

            return $tb <=> $ta;
        });

        return $all;
    }

    /**
     * 已开奖注单不再展示下注占用，避免与开奖结算同额时出现「扣两次」误解。
     *
     * @param  list<object>  $holdRows
     * @param  list<object>  $mergedSettlements
     * @param  array<int, array{actual_deduct_amount?: int}>  $ticketRefs
     * @return list<object>
     */
    private function holdsWithoutSettledMatch(
        array $holdRows,
        array $mergedSettlements,
        array $ticketRefs,
    ): array {
        if ($holdRows === [] || $mergedSettlements === []) {
            return $holdRows;
        }

        $sortedHolds = $holdRows;
        usort($sortedHolds, static function (object $a, object $b): int {
            $ta = $a->created_at ?? null;
            $tb = $b->created_at ?? null;
            if ($ta === null || $tb === null) {
                return (int) ($a->id ?? 0) <=> (int) ($b->id ?? 0);
            }

            return Carbon::parse($ta)->getTimestamp() <=> Carbon::parse($tb)->getTimestamp();
        });

        $consumedHoldIds = [];

        foreach ($mergedSettlements as $settlement) {
            $playerId = (int) ($settlement->player_id ?? 0);
            $ticketId = (int) ($settlement->ref_id ?? 0);
            $stake = $this->stakeMinorForSettlement($settlement, $ticketId, $ticketRefs);
            if ($playerId <= 0 || $stake <= 0) {
                continue;
            }

            $settledAt = $settlement->created_at ?? null;

            foreach ($sortedHolds as $hold) {
                $holdId = (int) ($hold->id ?? 0);
                if ($holdId <= 0 || isset($consumedHoldIds[$holdId])) {
                    continue;
                }

                if ((int) ($hold->player_id ?? 0) !== $playerId) {
                    continue;
                }

                if (abs((int) ($hold->amount ?? 0)) !== $stake) {
                    continue;
                }

                $holdAt = $hold->created_at ?? null;
                if ($holdAt !== null && $settledAt !== null
                    && Carbon::parse($holdAt)->gt(Carbon::parse($settledAt))) {
                    continue;
                }

                $consumedHoldIds[$holdId] = true;

                break;
            }
        }

        return array_values(array_filter(
            $holdRows,
            static fn (object $hold): bool => ! isset($consumedHoldIds[(int) ($hold->id ?? 0)]),
        ));
    }

    /**
     * @param  array<int, array{actual_deduct_amount?: int}>  $ticketRefs
     */
    private function stakeMinorForSettlement(object $settlement, int $ticketId, array $ticketRefs): int
    {
        $fromTicketRef = (int) ($ticketRefs[$ticketId]['actual_deduct_amount'] ?? 0);
        if ($fromTicketRef > 0) {
            return $fromTicketRef;
        }

        $amount = (int) ($settlement->amount ?? 0);
        if ($amount < 0) {
            return abs($amount);
        }

        return 0;
    }

    /**
     * @param  list<object>  $entries
     * @param  array<int, array{actual_deduct_amount?: int}>  $ticketRefs
     */
    private function mergeSettlementEntries(int $ticketId, array $entries, array $ticketRefs): ?object
    {
        $loss = null;
        $win = null;
        $release = null;
        $latestAt = null;

        foreach ($entries as $entry) {
            $reason = (string) ($entry->reason ?? '');
            if ($reason === 'game_settlement_loss') {
                $loss = $entry;
            } elseif ($reason === 'game_settlement_win') {
                $win = $entry;
            } elseif ($reason === 'bet_hold_release') {
                $release = $entry;
            }

            $at = $entry->created_at ?? null;
            if ($at !== null && ($latestAt === null || Carbon::parse($at)->gt(Carbon::parse($latestAt)))) {
                $latestAt = $at;
            }
        }

        $primary = $loss ?? $win ?? $release;
        if ($primary === null) {
            return null;
        }

        $signed = $loss !== null
            ? (int) $loss->amount
            : ($win !== null ? (int) $win->amount : 0);

        return (object) [
            'id' => (int) ($loss->id ?? $win->id ?? $release->id ?? 0),
            'amount' => $signed,
            'reason' => self::DISPLAY_GAME_SETTLEMENT,
            'ref_type' => 'ticket_item',
            'ref_id' => $ticketId,
            'created_at' => $latestAt ?? $primary->created_at,
            'player_id' => $primary->player_id ?? null,
            'site_code' => $primary->site_code ?? null,
            'site_player_id' => $primary->site_player_id ?? null,
            'username' => $primary->username ?? null,
            'nickname' => $primary->nickname ?? null,
            'agent_node_id' => $primary->agent_node_id ?? null,
            'funding_mode' => $primary->funding_mode ?? null,
            'auth_source' => $primary->auth_source ?? null,
            'default_currency' => $primary->default_currency ?? null,
            'direct_agent_id' => $primary->direct_agent_id ?? null,
            'direct_agent_code' => $primary->direct_agent_code ?? null,
            'direct_agent_name' => $primary->direct_agent_name ?? null,
            'parent_agent_id' => $primary->parent_agent_id ?? null,
            'parent_agent_code' => $primary->parent_agent_code ?? null,
            'parent_agent_name' => $primary->parent_agent_name ?? null,
            'stake_minor' => (int) ($ticketRefs[$ticketId]['actual_deduct_amount'] ?? abs($signed)),
        ];
    }

    private function ticketItemId(object $row): int
    {
        if ((string) ($row->ref_type ?? '') !== 'ticket_item') {
            return 0;
        }

        return (int) ($row->ref_id ?? 0);
    }
}
