<?php

declare(strict_types=1);

namespace app\common\library\game;

use InvalidArgumentException;

/**
 * 移动端支付/收款展示（game_config.finance_cashier，含 channels 充值渠道）
 *
 * 货币列表每行含：
 * - deposit_coins_per_fiat：充值参考，每支付 1 单位该货币到账的平台币数量
 * - withdraw_coins_per_fiat：提现换算，每兑换 1 单位该货币所需平台币数量
 */
final class FinanceCashierConfig
{
    public const CONFIG_KEY = 'finance_cashier';

    /**
     * @return array<string, mixed>
     */
    public static function defaultPayload(): array
    {
        return [
            'platform_coin' => [
                'label_zh' => '钻石',
                'label_en' => 'Diamonds',
            ],
            'currencies' => [
                [
                    'code'                    => 'MYR',
                    'label_zh'                => '马来西亚林吉特',
                    'label_en'                => 'Malaysian Ringgit',
                    'sort'                    => 10,
                    'deposit_coins_per_fiat'  => '100',
                    'withdraw_coins_per_fiat' => '100',
                ],
                [
                    'code'                    => 'THB',
                    'label_zh'                => '泰铢',
                    'label_en'                => 'Thai Baht',
                    'sort'                    => 20,
                    'deposit_coins_per_fiat'  => '100',
                    'withdraw_coins_per_fiat' => '100',
                ],
            ],
            'deposit_banks' => [
                ['currency_code' => 'MYR', 'code' => 'pbb', 'name_zh' => 'Public Bank', 'name_en' => 'Public Bank', 'sort' => 10],
                ['currency_code' => 'MYR', 'code' => 'mbb', 'name_zh' => 'Maybank2U', 'name_en' => 'Maybank2U', 'sort' => 20],
                ['currency_code' => 'THB', 'code' => '106', 'name_zh' => 'BANGKOK BANK PUBLIC COMPANY LTD.', 'name_en' => 'BANGKOK BANK PUBLIC COMPANY LTD.', 'sort' => 10],
                ['currency_code' => 'THB', 'code' => '107', 'name_zh' => 'KASIKORNBANK PUBLIC COMPANY LIMITED', 'name_en' => 'KASIKORNBANK PUBLIC COMPANY LIMITED', 'sort' => 20],
            ],
            'withdraw_banks' => [
                ['currency_code' => 'MYR', 'code' => 'pbb', 'name_zh' => 'Public Bank', 'name_en' => 'Public Bank', 'sort' => 10],
                ['currency_code' => 'MYR', 'code' => 'mbb', 'name_zh' => 'Maybank2U', 'name_en' => 'Maybank2U', 'sort' => 20],
            ],
            'withdraw_limits' => [
                'min_ewallet' => '10',
                'min_bank' => '10',
            ],
            'withdraw_copy' => [
                'rate_hint_zh' => '汇率为参考价格，实际以提现时为准。',
                'rate_hint_en' => 'Exchange rates are for reference only; the actual rate at withdrawal time prevails.',
                'processing_zh' => '9秒即可到账',
                'processing_en' => 'Arrives in seconds',
                'fee_note_zh' => '注意：RM 10 — RM 99.99 之间的交易将收取最低 RM 1 的提现手续费。',
                'fee_note_en' => 'A minimum RM 1 handling fee may apply for withdrawals between RM 10 and RM 99.99.',
                'rate_mode' => 'fixed',
            ],
            'channels' => [],
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public static function parseFromConfigValue(mixed $raw): array
    {
        $out = self::defaultPayload();
        if (!is_string($raw) || trim($raw) === '') {
            return self::normalizePayload($out, []);
        }
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return self::normalizePayload($out, []);
        }
        if (isset($decoded['platform_coin']) && is_array($decoded['platform_coin'])) {
            $out['platform_coin'] = array_replace($out['platform_coin'], array_intersect_key($decoded['platform_coin'], $out['platform_coin']));
        }
        $legacyRates = [];
        if (isset($decoded['rates']) && is_array($decoded['rates'])) {
            $legacyRates = array_values($decoded['rates']);
        }
        foreach (['currencies', 'deposit_banks', 'withdraw_banks', 'channels'] as $listKey) {
            if (isset($decoded[$listKey]) && is_array($decoded[$listKey])) {
                $out[$listKey] = array_values($decoded[$listKey]);
            }
        }
        if (isset($decoded['withdraw_limits']) && is_array($decoded['withdraw_limits'])) {
            $out['withdraw_limits'] = array_replace($out['withdraw_limits'], array_intersect_key($decoded['withdraw_limits'], $out['withdraw_limits']));
        }
        if (isset($decoded['withdraw_copy']) && is_array($decoded['withdraw_copy'])) {
            $out['withdraw_copy'] = array_replace($out['withdraw_copy'], array_intersect_key($decoded['withdraw_copy'], $out['withdraw_copy']));
        }

        return self::normalizePayload($out, $legacyRates);
    }

    public static function encodeForDb(array $payload): string
    {
        $legacyRates = [];
        if (isset($payload['rates']) && is_array($payload['rates'])) {
            $legacyRates = array_values($payload['rates']);
        }
        $normalized = self::normalizePayload($payload, $legacyRates);
        $normalized['channels'] = DepositChannel::prepareOverridesForSave(
            DepositChannel::expandRowsForAdmin($normalized['channels'] ?? [])
        );
        self::validate($normalized);

        return json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    /**
     * @param array<string, mixed> $p
     * @param list<array<string, mixed>> $legacyRates
     *
     * @return array<string, mixed>
     */
    private static function normalizePayload(array $p, array $legacyRates): array
    {
        $defaults = self::defaultPayload();
        $out = array_replace($defaults, $p);
        unset($out['rates'], $out['fx_pairs']);

        $withdrawFromLegacyRates = [];
        foreach ($legacyRates as $r) {
            if (!is_array($r)) {
                continue;
            }
            $cur = isset($r['currency']) && is_string($r['currency']) ? strtoupper(trim($r['currency'])) : '';
            $ratio = self::ratioStringFromRow($r, 'diamonds_per_fiat_unit');
            if ($cur !== '' && $ratio !== '') {
                $withdrawFromLegacyRates[$cur] = $ratio;
            }
        }

        if (isset($out['platform_coin']) && is_array($out['platform_coin'])) {
            $out['platform_coin'] = array_replace($defaults['platform_coin'], $out['platform_coin']);
        }
        if (isset($out['currencies']) && is_array($out['currencies'])) {
            foreach ($out['currencies'] as $i => $row) {
                if (!is_array($row)) {
                    unset($out['currencies'][$i]);
                    continue;
                }
                $code = isset($row['code']) && is_string($row['code']) ? strtoupper(trim($row['code'])) : '';
                $dep = self::ratioStringFromRow($row, 'deposit_coins_per_fiat');
                $wdr = self::ratioStringFromRow($row, 'withdraw_coins_per_fiat');
                if ($wdr === '') {
                    $wdr = self::ratioStringFromRow($row, 'diamonds_per_fiat_unit');
                }
                if ($wdr === '' && $code !== '' && isset($withdrawFromLegacyRates[$code])) {
                    $wdr = $withdrawFromLegacyRates[$code];
                }
                if ($dep === '' && $wdr !== '') {
                    $dep = $wdr;
                }
                if ($wdr === '' && $dep !== '') {
                    $wdr = $dep;
                }
                if ($code !== '' && $dep === '' && $wdr === '') {
                    $dep = '100';
                    $wdr = '100';
                }
                $out['currencies'][$i] = [
                    'code'                    => $code,
                    'label_zh'                => isset($row['label_zh']) && is_string($row['label_zh']) ? trim($row['label_zh']) : '',
                    'label_en'                => isset($row['label_en']) && is_string($row['label_en']) ? trim($row['label_en']) : '',
                    'sort'                    => self::normalizeSort($row['sort'] ?? 0),
                    'deposit_coins_per_fiat'  => $dep,
                    'withdraw_coins_per_fiat' => $wdr,
                ];
            }
            $out['currencies'] = array_values(array_filter($out['currencies'], static fn ($r) => is_array($r) && $r['code'] !== ''));
        }

        usort($out['currencies'], static function (array $a, array $b): int {
            $sa = $a['sort'] ?? 0;
            $sb = $b['sort'] ?? 0;
            if ($sa !== $sb) {
                return $sa <=> $sb;
            }
            $ca = $a['code'] ?? '';
            $cb = $b['code'] ?? '';

            return strcmp($ca, $cb);
        });

        $out['deposit_banks'] = self::normalizeBanksByCurrency(
            isset($out['deposit_banks']) && is_array($out['deposit_banks']) ? $out['deposit_banks'] : []
        );
        $out['withdraw_banks'] = self::normalizeBanksByCurrency(
            isset($out['withdraw_banks']) && is_array($out['withdraw_banks']) ? $out['withdraw_banks'] : []
        );
        if (isset($out['withdraw_limits']) && is_array($out['withdraw_limits'])) {
            $wl = array_replace($defaults['withdraw_limits'], $out['withdraw_limits']);
            foreach (['min_ewallet', 'min_bank'] as $k) {
                if (isset($wl[$k]) && is_numeric($wl[$k]) && !is_string($wl[$k])) {
                    $wl[$k] = strval($wl[$k]);
                }
                if (!isset($wl[$k]) || !is_string($wl[$k])) {
                    $wl[$k] = $defaults['withdraw_limits'][$k];
                }
            }
            $out['withdraw_limits'] = $wl;
        }
        if (isset($out['withdraw_copy']) && is_array($out['withdraw_copy'])) {
            $out['withdraw_copy'] = array_replace($defaults['withdraw_copy'], array_intersect_key($out['withdraw_copy'], $defaults['withdraw_copy']));
            $mode = isset($out['withdraw_copy']['rate_mode']) && is_string($out['withdraw_copy']['rate_mode'])
                ? strtolower(trim($out['withdraw_copy']['rate_mode']))
                : 'fixed';
            if (!in_array($mode, ['fixed', 'live'], true)) {
                $mode = 'fixed';
            }
            $out['withdraw_copy']['rate_mode'] = $mode;
        }
        if (isset($out['channels']) && is_array($out['channels'])) {
            $out['channels'] = DepositChannel::normalizeOverrides(array_values($out['channels']));
        } else {
            $out['channels'] = [];
        }
        usort($out['channels'], static function (array $a, array $b): int {
            $sa = $a['sort'] ?? 0;
            $sb = $b['sort'] ?? 0;
            if ($sa !== $sb) {
                return $sa <=> $sb;
            }

            return strcmp($a['code'] ?? '', $b['code'] ?? '');
        });

        unset($out['fx_pairs']);
        // 历史 JSON 可能含 withdraw_fields；已废弃，不再落库与返回给后台表单
        unset($out['withdraw_fields']);

        return $out;
    }

    /**
     * @param array<string, mixed> $row
     */
    private static function ratioStringFromRow(array $row, string $key): string
    {
        if (!isset($row[$key])) {
            return '';
        }
        if (is_string($row[$key])) {
            return trim($row[$key]);
        }
        if (is_numeric($row[$key])) {
            return strval($row[$key]);
        }

        return '';
    }

    private static function normalizeSort(mixed $v): int
    {
        $opt = filter_var($v, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 99999]]);
        if ($opt !== false) {
            return $opt;
        }
        if (is_numeric($v)) {
            $f = filter_var($v, FILTER_VALIDATE_FLOAT);
            if ($f !== false) {
                $rounded = round($f);
                $opt2 = filter_var($rounded, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 99999]]);
                if ($opt2 !== false) {
                    return $opt2;
                }
            }
        }

        return 0;
    }

    /**
     * @param list<mixed> $rows
     * @return list<array{currency_code: string, code: string, name_zh: string, name_en: string, sort: int}>
     */
    private static function normalizeBanksByCurrency(array $rows): array
    {
        $out = [];
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $currencyCode = isset($row['currency_code']) && is_string($row['currency_code']) ? strtoupper(trim($row['currency_code'])) : '';
            if ($currencyCode === '') {
                $currencyCode = 'MYR';
            }
            $codeRaw = $row['code'] ?? '';
            $code = '';
            if (is_string($codeRaw)) {
                $code = strtolower(trim($codeRaw));
            } elseif (is_numeric($codeRaw)) {
                $code = strtolower(trim(strval($codeRaw)));
            }
            $out[] = [
                'currency_code' => $currencyCode,
                'code'          => $code,
                'name_zh'       => isset($row['name_zh']) && is_string($row['name_zh']) ? trim($row['name_zh']) : '',
                'name_en'       => isset($row['name_en']) && is_string($row['name_en']) ? trim($row['name_en']) : '',
                'sort'          => self::normalizeSort($row['sort'] ?? 0),
            ];
        }
        $out = array_values(array_filter($out, static fn ($r) => is_array($r) && $r['currency_code'] !== '' && $r['code'] !== ''));
        usort($out, static function (array $a, array $b): int {
            $ca = isset($a['currency_code']) && is_string($a['currency_code']) ? $a['currency_code'] : '';
            $cb = isset($b['currency_code']) && is_string($b['currency_code']) ? $b['currency_code'] : '';
            if ($ca !== $cb) {
                return strcmp($ca, $cb);
            }
            $sa = isset($a['sort']) && is_numeric($a['sort']) ? intval(strval($a['sort'])) : 0;
            $sb = isset($b['sort']) && is_numeric($b['sort']) ? intval(strval($b['sort'])) : 0;
            if ($sa !== $sb) {
                return $sa <=> $sb;
            }
            $ka = isset($a['code']) && is_string($a['code']) ? $a['code'] : '';
            $kb = isset($b['code']) && is_string($b['code']) ? $b['code'] : '';

            return strcmp($ka, $kb);
        });

        return $out;
    }

    /**
     * @param array<string, mixed> $p
     */
    private static function validate(array $p): void
    {
        if (!isset($p['platform_coin']) || !is_array($p['platform_coin'])) {
            throw new InvalidArgumentException('platform_coin format error');
        }
        $lz = $p['platform_coin']['label_zh'] ?? '';
        $le = $p['platform_coin']['label_en'] ?? '';
        if (!is_string($lz) || trim($lz) === '' || !is_string($le) || trim($le) === '') {
            throw new InvalidArgumentException('Platform coin labels (zh/en) are required');
        }
        if (!isset($p['currencies']) || !is_array($p['currencies']) || $p['currencies'] === []) {
            throw new InvalidArgumentException('At least one currency is required');
        }
        $seenCodes = [];
        foreach ($p['currencies'] as $idx => $row) {
            if (!is_array($row)) {
                throw new InvalidArgumentException('Currency row format error');
            }
            $code = $row['code'] ?? '';
            if (!is_string($code) || !preg_match('/^[A-Z0-9]{2,12}$/', $code)) {
                throw new InvalidArgumentException('Currency code is invalid');
            }
            if (isset($seenCodes[$code])) {
                throw new InvalidArgumentException('Duplicate currency code');
            }
            $seenCodes[$code] = true;
            $dep = $row['deposit_coins_per_fiat'] ?? '';
            $wdr = $row['withdraw_coins_per_fiat'] ?? '';
            if (!is_string($dep) || $dep === '' || !is_numeric($dep) || bccomp($dep, '0', 2) <= 0) {
                throw new InvalidArgumentException('Deposit rate must be a number greater than 0');
            }
            if (!is_string($wdr) || $wdr === '' || !is_numeric($wdr) || bccomp($wdr, '0', 2) <= 0) {
                throw new InvalidArgumentException('Withdraw rate must be a number greater than 0');
            }
        }

        // 校验 deposit channels 的币种白名单：currency_codes 仅允许来自 currencies
        if (isset($p['channels']) && is_array($p['channels'])) {
            foreach ($p['channels'] as $row) {
                if (!is_array($row)) {
                    continue;
                }
                $status = isset($row['status']) && is_numeric($row['status']) ? intval($row['status']) : 0;
                $cc = array_key_exists('currency_codes', $row) ? ($row['currency_codes'] ?? null) : null;
                if ($cc === null) {
                    continue; // null => 兼容全部币种（历史配置默认行为）
                }
                if (!is_array($cc)) {
                    throw new InvalidArgumentException('Channel currency_codes format error');
                }
                if ($status === 1 && $cc === []) {
                    throw new InvalidArgumentException('Enabled channel currency_codes can not be empty');
                }
                foreach ($cc as $c) {
                    if (!is_string($c) || !preg_match('/^[A-Z0-9]{2,12}$/', $c)) {
                        throw new InvalidArgumentException('Channel currency_codes contains invalid currency code');
                    }
                    if (!isset($seenCodes[$c])) {
                        throw new InvalidArgumentException('Channel currency_codes contains currency not configured');
                    }
                }
            }
        }

        foreach (['deposit_banks', 'withdraw_banks'] as $bankKey) {
            if (!isset($p[$bankKey]) || !is_array($p[$bankKey])) {
                continue;
            }
            $seen = [];
            foreach ($p[$bankKey] as $row) {
                if (!is_array($row)) {
                    throw new InvalidArgumentException('Bank row format error');
                }
                $currencyCode = $row['currency_code'] ?? '';
                if (!is_string($currencyCode) || !isset($seenCodes[$currencyCode])) {
                    throw new InvalidArgumentException('Bank currency_code is invalid');
                }
                $code = $row['code'] ?? '';
                if (!is_string($code) || !preg_match('/^[a-z0-9][a-z0-9_\-]{0,31}$/', $code)) {
                    throw new InvalidArgumentException('Bank code is invalid');
                }
                $uniq = $currencyCode . '|' . $code;
                if (isset($seen[$uniq])) {
                    throw new InvalidArgumentException('Duplicate bank code');
                }
                $seen[$uniq] = true;
            }
        }
        if (isset($p['withdraw_limits']) && is_array($p['withdraw_limits'])) {
            foreach (['min_ewallet', 'min_bank'] as $k) {
                $v = $p['withdraw_limits'][$k] ?? '0';
                if (!is_string($v) || !is_numeric($v) || bccomp($v, '0', 2) < 0) {
                    throw new InvalidArgumentException('Withdraw min limit must be a number not less than 0');
                }
            }
        }
        $reg = DepositChannel::codeRegistry();
        if ($reg !== [] && (!isset($p['channels']) || !is_array($p['channels']) || $p['channels'] === [])) {
            throw new InvalidArgumentException('Please configure deposit channels');
        }
    }
}
