<?php

namespace App\Services\Ticket;

use App\Models\OddsItem;
use App\Lottery\ErrorCode;
use App\Models\PlayConfigItem;
use Illuminate\Support\Collection;
use App\Exceptions\TicketOperationException;

final class PlayRuleEngine
{
    public function __construct(
        private readonly NumberNormalizer $normalizer,
    ) {}

    /**
     * @param  array<string, mixed>  $line
     * @param  Collection<int, OddsItem>  $oddsItems
     * @return array<string, mixed>
     */
    public function evaluateLine(array $line, PlayConfigItem $playConfig, Collection $oddsItems): array
    {
        $playCode = (string) $line['play_code'];
        $dimension = $line['dimension'] ?? null;
        $digitSlot = $line['digit_slot'] ?? null;
        $amount = (int) $line['amount'];
        $number = $this->normalizer->normalize($playCode, (string) $line['number'], is_string($dimension) ? $dimension : null);

        if ($amount < (int) $playConfig->min_bet_amount || $amount > (int) $playConfig->max_bet_amount) {
            throw new TicketOperationException('bet_amount_out_of_range', ErrorCode::WalletAmountExceedsLimit->value);
        }

        if (in_array($playCode, ['odd', 'even', 'digit_big', 'digit_small'], true) && ! is_string($dimension)) {
            throw new TicketOperationException('dimension_required', ErrorCode::BetInvalidPlayInput->value);
        }

        if (in_array($playCode, ['digit_big', 'digit_small'], true) && ! is_int($digitSlot) && ! ctype_digit((string) $digitSlot)) {
            throw new TicketOperationException('digit_slot_required', ErrorCode::BetInvalidPlayInput->value);
        }

        $digitSlotInt = $digitSlot === null ? null : (int) $digitSlot;
        $combos = $this->expandToCombinations($playCode, $number, is_string($dimension) ? $dimension : null, $digitSlotInt);
        $combinationCount = count($combos);
        if ($combinationCount < 1) {
            throw new TicketOperationException('empty_combinations', ErrorCode::BetInvalidPlayInput->value);
        }

        $unitBetAmount = $this->resolveUnitBetAmount($playCode, $amount, $combinationCount);
        $totalBetAmount = $this->resolveTotalBetAmount($playCode, $amount, $unitBetAmount, $combinationCount);
        $dimensionInt = $this->toDimensionInt(is_string($dimension) ? $dimension : null, $playConfig);
        $primaryOdds = $this->pickPrimaryOdds($oddsItems);
        $rebateRate = (float) $primaryOdds->rebate_rate;
        // 佣金按维度（2D/3D/4D）配置：从 odds_items 中查找匹配维度的佣金率
        $commissionRate = $this->pickCommissionByDimension($oddsItems, $dimensionInt);
        $actualDeductAmount = max(0, (int) floor($totalBetAmount * (1 - $rebateRate)));
        $maxOdds = $oddsItems->max(fn (OddsItem $row) => (int) $row->odds_value) ?? 0;
        $estimatedPayoutPerCombo = (int) floor($unitBetAmount * ($maxOdds / 10000));
        $estimatedMaxPayout = $estimatedPayoutPerCombo * $combinationCount;

        return [
            'original_number' => (string) $line['number'],
            'normalized_number' => $number,
            'play_code' => $playCode,
            'dimension' => $this->toDimensionInt(is_string($dimension) ? $dimension : null, $playConfig),
            'digit_slot' => $digitSlotInt,
            'bet_mode' => $playConfig->bet_mode,
            'unit_bet_amount' => $unitBetAmount,
            'total_bet_amount' => $totalBetAmount,
            'rebate_rate_snapshot' => number_format($rebateRate, 4, '.', ''),
            'commission_rate_snapshot' => number_format($commissionRate, 4, '.', ''),
            'actual_deduct_amount' => $actualDeductAmount,
            'combination_count' => $combinationCount,
            'estimated_max_payout' => $estimatedMaxPayout,
            'odds_snapshot_json' => $oddsItems->map(fn (OddsItem $row) => [
                'prize_scope' => $row->prize_scope,
                'odds_value' => (int) $row->odds_value,
                'rebate_rate' => (string) $row->rebate_rate,
                'commission_rate' => (string) $row->commission_rate,
            ])->values()->all(),
            'rule_snapshot_json' => [
                'play_code' => $playCode,
                'dimension' => $dimension,
                'digit_slot' => $digitSlotInt,
                'combination_count' => $combinationCount,
                'rounding_refund_amount' => $playCode === 'mbox'
                    ? max(0, $amount - $totalBetAmount)
                    : 0,
            ],
            'combinations' => collect($combos)->values()->map(function (string $combo, int $index) use ($unitBetAmount, $estimatedPayoutPerCombo): array {
                return [
                    'combination_no' => $index + 1,
                    'number_4d' => $combo,
                    'bet_amount' => $unitBetAmount,
                    'estimated_payout' => $estimatedPayoutPerCombo,
                ];
            })->all(),
        ];
    }

    /**
     * @return list<string>
     */
    private function expandToCombinations(string $playCode, string $number, ?string $dimension, ?int $digitSlot): array
    {
        return match ($playCode) {
            'big', 'small', 'pos_4a', 'pos_4b', 'pos_4c', 'pos_4d', 'pos_4e', 'straight' => [$number],
            'ibox', 'mbox', 'box' => $this->uniquePermutations($number),
            'roll' => $this->expandRoll($number),
            'pos_3a', 'pos_3b', 'pos_3c', 'pos_3abc' => $this->expandSuffix($number, 3),
            'pos_2a', 'pos_2b', 'pos_2c', 'pos_2abc' => $this->expandSuffix($number, 2),
            'head' => $this->expandHeadTail(true),
            'tail' => $this->expandHeadTail(false),
            'odd' => $this->expandOddEven($dimension, true),
            'even' => $this->expandOddEven($dimension, false),
            'digit_big' => $this->expandDigitSize($dimension, $digitSlot, true),
            'digit_small' => $this->expandDigitSize($dimension, $digitSlot, false),
            default => throw new TicketOperationException('unsupported_play_expand', ErrorCode::BetPlayUnsupported->value),
        };
    }

    /**
     * @return list<string>
     */
    private function uniquePermutations(string $digits): array
    {
        $results = [];
        $this->permute(str_split($digits), 0, $results);
        $values = array_values(array_unique($results));
        sort($values);

        return $values;
    }

    /**
     * @param  array<int, string>  $chars
     * @param  array<int, string>  $results
     */
    private function permute(array $chars, int $index, array &$results): void
    {
        if ($index === count($chars) - 1) {
            $results[] = implode('', $chars);

            return;
        }

        $seen = [];
        for ($i = $index; $i < count($chars); $i++) {
            if (isset($seen[$chars[$i]])) {
                continue;
            }
            $seen[$chars[$i]] = true;
            [$chars[$index], $chars[$i]] = [$chars[$i], $chars[$index]];
            $this->permute($chars, $index + 1, $results);
        }
    }

    /**
     * @return list<string>
     */
    private function expandRoll(string $pattern): array
    {
        $results = [''];
        foreach (str_split($pattern) as $char) {
            $next = [];
            if ($char === 'R') {
                foreach ($results as $prefix) {
                    for ($i = 0; $i <= 9; $i++) {
                        $next[] = $prefix.$i;
                    }
                }
            } else {
                foreach ($results as $prefix) {
                    $next[] = $prefix.$char;
                }
            }
            $results = $next;
        }

        return $results;
    }

    /**
     * @return list<string>
     */
    private function expandSuffix(string $suffix, int $length): array
    {
        $results = [];
        $prefixLength = 4 - $length;
        $max = 10 ** $prefixLength;
        for ($i = 0; $i < $max; $i++) {
            $results[] = str_pad((string) $i, $prefixLength, '0', STR_PAD_LEFT).$suffix;
        }

        return $results;
    }

    /**
     * @return list<string>
     */
    private function expandHeadTail(bool $isHead): array
    {
        $results = [];
        $start = $isHead ? 5 : 0;
        $end = $isHead ? 9 : 4;
        for ($first = $start; $first <= $end; $first++) {
            for ($rest = 0; $rest < 1000; $rest++) {
                $results[] = $first.str_pad((string) $rest, 3, '0', STR_PAD_LEFT);
            }
        }

        return $results;
    }

    /**
     * @return list<string>
     */
    private function expandOddEven(?string $dimension, bool $odd): array
    {
        $digits = $odd ? ['1', '3', '5', '7', '9'] : ['0', '2', '4', '6', '8'];
        $suffixLength = match ($dimension) {
            'D2' => 2,
            'D3' => 3,
            default => 4,
        };
        $prefixLength = 4 - $suffixLength;
        $prefixMax = 10 ** max($prefixLength, 0);
        $middleLength = max($suffixLength - 1, 0);
        $middleMax = 10 ** $middleLength;
        $results = [];

        for ($prefix = 0; $prefix < $prefixMax; $prefix++) {
            for ($mid = 0; $mid < $middleMax; $mid++) {
                foreach ($digits as $last) {
                    $results[] = str_pad((string) $prefix, $prefixLength, '0', STR_PAD_LEFT)
                        .str_pad((string) $mid, $middleLength, '0', STR_PAD_LEFT)
                        .$last;
                }
            }
        }

        return $results;
    }

    /**
     * @return list<string>
     */
    private function expandDigitSize(?string $dimension, ?int $digitSlot, bool $isBig): array
    {
        if ($digitSlot === null) {
            throw new TicketOperationException('digit_slot_missing', ErrorCode::BetInvalidPlayInput->value);
        }

        $validSlots = match ($dimension) {
            'D2' => [2, 3],
            'D3' => [1, 2, 3],
            default => [0, 1, 2, 3],
        };
        if (! in_array($digitSlot, $validSlots, true)) {
            throw new TicketOperationException('digit_slot_invalid', ErrorCode::BetInvalidPlayInput->value);
        }

        $targetDigits = $isBig ? ['5', '6', '7', '8', '9'] : ['0', '1', '2', '3', '4'];
        $results = [];
        for ($i = 0; $i < 10000; $i++) {
            $number = str_pad((string) $i, 4, '0', STR_PAD_LEFT);
            if (in_array($number[$digitSlot], $targetDigits, true)) {
                $results[] = $number;
            }
        }

        return $results;
    }

    private function resolveUnitBetAmount(string $playCode, int $amount, int $combinationCount): int
    {
        return match ($playCode) {
            'mbox' => max(0, intdiv($amount, $combinationCount)),
            default => $amount,
        };
    }

    private function resolveTotalBetAmount(string $playCode, int $rawAmount, int $unitBetAmount, int $combinationCount): int
    {
        return match ($playCode) {
            'ibox', 'roll' => $rawAmount * $combinationCount,
            'mbox' => $unitBetAmount * $combinationCount,
            default => $rawAmount,
        };
    }

    /**
     * @param  Collection<int, OddsItem>  $oddsItems
     */
    private function pickPrimaryOdds(Collection $oddsItems): OddsItem
    {
        foreach (['first', 'default', 'second', 'third', 'starter', 'consolation'] as $scope) {
            $hit = $oddsItems->firstWhere('prize_scope', $scope);
            if ($hit !== null) {
                return $hit;
            }
        }

        return $oddsItems->firstOrFail();
    }

    /**
     * 按维度（2D/3D/4D）获取佣金率
     * 
     * @param  Collection<int, OddsItem>  $oddsItems
     */
    private function pickCommissionByDimension(Collection $oddsItems, ?int $dimension): float
    {
        if ($dimension === null) {
            // 如果维度为空，返回第一个 odds item 的佣金率（向后兼容）
            return (float) ($oddsItems->first()?->commission_rate ?? 0);
        }

        // 查找匹配维度的 odds item
        $dimensionItem = $oddsItems->firstWhere('dimension', $dimension);
        if ($dimensionItem !== null) {
            return (float) $dimensionItem->commission_rate;
        }

        // 如果没有找到匹配维度的，返回第一个的佣金率（向后兼容）
        return (float) ($oddsItems->first()?->commission_rate ?? 0);
    }

    private function toDimensionInt(?string $dimension, PlayConfigItem $playConfig): ?int
    {
        return match ($dimension) {
            'D2' => 2,
            'D3' => 3,
            'D4' => 4,
            default => $playConfig->dimension === null ? null : (int) $playConfig->dimension,
        };
    }
}
