<?php

namespace App\Services;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use App\Models\Player;
use App\Lottery\ErrorCode;
use Illuminate\Http\Request;
use App\Support\PlayerAuthSource;
use App\Support\PlayerAutoRegistrationDefaults;
use App\Support\PlayerFundingMode;
use App\Support\PlayerTokenAesUnwrap;
use Illuminate\Database\QueryException;
use App\Exceptions\PlayerAuthenticationException;
use App\Services\Integration\PartnerSiteConfigResolver;

/**
 * 从请求头解析玩家身份，返回已落库的 {@see Player}。
 *
 * ## Dev bypass（开发绕过）与正式 JWT 的互斥关系
 *
 * | 条件 | Authorization 示例 | 行为 |
 * | :--- | :--- | :--- |
 * | `LOTTERY_PLAYER_AUTH_DEV_BYPASS=true` 且 `APP_ENV` 为 `local` 或 `testing`，且 token 以 `dev:` 开头 | `Bearer dev:12` | **仅走 dev**：按 `players.id` 查库，不验 JWT，不要求配置 `MAIN_SITE_SSO_JWT_SECRET`。 |
 * | 同上环境且开关为 true，但 token **不是** `dev:` | `Bearer eyJ...` | **走正式 JWT**：验签、建档逻辑见下；`dev:` 前缀不会当作 JWT 解析。 |
 * | `dev_bypass` 为 false 或非 local/testing | `Bearer dev:12` | **不走 dev**：整段当作 JWT 解码，必然失败（8002）。 |
 * | 任意环境 | `Bearer eyJ...` | **走正式 JWT**：必须配置 `MAIN_SITE_SSO_JWT_SECRET`；验签成功后按 `site_code` + `site_player_id` **首次自动建档**（PRD），并刷新 `last_login_at`。 |
 *
 * ## 正式 JWT（主站 SSO）
 *
 * - 成功验签后：若库中无映射行则 `firstOrCreate`（默认币种 `lottery.default_currency`、status=active）。
 * - 并发首登可能触发唯一键冲突，会捕获后重查。
 * - 解析成功后校验 {@see Player::status}：非 active 时抛 {@see ErrorCode::PlayerAccountSuspended}（HTTP 403）。
 *
 * 错误码见 {@see ErrorCode} 玩家 SSO 段（8001–8005）。
 */
final class PlayerTokenResolver
{
    /** players.status：与迁移注释一致 */
    private const PLAYER_STATUS_ACTIVE = 0;

    public function __construct(
        private readonly PartnerSiteConfigResolver $partnerSiteConfigResolver,
    ) {}

    public function resolve(Request $request): Player
    {
        $header = $request->header('Authorization', '');
        if (! is_string($header) || ! str_starts_with($header, 'Bearer ')) {
            throw new PlayerAuthenticationException(
                '缺少或非法 Authorization',
                ErrorCode::PlayerAuthorizationInvalid->value,
            );
        }

        // 标准：`Authorization: Bearer <token>`，此处去掉前缀 7 字节 "Bearer "
        $token = trim(substr($header, 7));
        if ($token === '') {
            throw new PlayerAuthenticationException('Token 为空', ErrorCode::PlayerAuthorizationInvalid->value);
        }

        // 仅当 dev bypass 开启且 token 形如 dev:{id} 时走开发分支；否则一律按 JWT 处理
        if ($this->devBypassAllowed() && str_starts_with($token, 'dev:')) {
            $player = $this->resolveDevToken($token);
        } else {
            $jwtPlain = $this->unwrapOpaqueToJwtString($token);
            if ($this->peekAuthSourceFromJwt($jwtPlain) === PlayerAuthSource::LOTTERY_NATIVE) {
                $player = $this->resolveNativeJwt($jwtPlain);
            } else {
                $siteCode = $this->partnerSiteConfigResolver->peekSiteCodeFromJwt($jwtPlain);
                if ($siteCode === null) {
                    throw new PlayerAuthenticationException('JWT 缺少站点标识', ErrorCode::PlayerTokenInvalid->value);
                }

                $siteConfig = $this->partnerSiteConfigResolver->resolveBySiteCode($siteCode);
                if (! $siteConfig->enabled) {
                    throw new PlayerAuthenticationException('站点已停用', ErrorCode::PlayerAccountSuspended->value, 403);
                }

                $secret = $siteConfig->ssoJwtSecret;
                if (! is_string($secret) || $secret === '') {
                    throw new PlayerAuthenticationException(
                        'SSO 未配置（站点 '.$siteCode.'）',
                        ErrorCode::PlayerSsoSecretNotConfigured->value,
                        503,
                    );
                }

                $player = $this->resolveSsoJwt($jwtPlain, $secret);
            }
        }

        $this->assertPlayerActive($player);

        // 正式 JWT 已在 resolveJwt 内写入 last_login_at；dev 仅在此处写入
        if ($this->devBypassAllowed() && str_starts_with($token, 'dev:')) {
            $player->forceFill(['last_login_at' => now()])->save();

            return $player->refresh();
        }

        return $player;
    }

    private function devBypassAllowed(): bool
    {
        return (bool) config('lottery.player_auth.dev_bypass')
            && app()->environment(['local', 'testing']);
    }

    private function resolveDevToken(string $token): Player
    {
        if (! preg_match('/^dev:(\d+)$/', $token, $m)) {
            throw new PlayerAuthenticationException(
                '开发 Token 格式应为 dev:{玩家ID}',
                ErrorCode::PlayerTokenInvalid->value,
            );
        }

        $player = Player::query()->find((int) $m[1]);
        if ($player === null) {
            throw new PlayerAuthenticationException('玩家不存在', ErrorCode::PlayerNotRegistered->value);
        }

        return $player;
    }

    /**
     * URL/Query Opaque：支持内联 JWT，或 AES-GCM 包裹的内层 JWT（见 {@see PlayerTokenAesUnwrap}）。
     */
    private function resolveJwtOrAesWrappedJwt(string $opaque, string $secret): Player
    {
        $jwtPlain = $this->unwrapOpaqueToJwtString($opaque);

        return $this->resolveSsoJwt($jwtPlain, $secret);
    }

    /**
     * 已是 `xxx.yyy.zzz` 则视为明文 JWT；否则在配置了 AES 密钥时尝试解包。
     */
    private function unwrapOpaqueToJwtString(string $token): string
    {
        $trimmed = trim($token);
        if ($this->looksLikeCompactJwt($trimmed)) {
            return $trimmed;
        }

        $inner = PlayerTokenAesUnwrap::tryUnwrap($trimmed);

        return $inner ?? $trimmed;
    }

    private function looksLikeCompactJwt(string $token): bool
    {
        return preg_match('/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/', $token) === 1;
    }

    private function resolveNativeJwt(string $jwt): Player
    {
        $secret = (string) config('lottery.player_auth.native.secret', '');
        if ($secret === '') {
            throw new PlayerAuthenticationException(
                '原生登录未配置',
                ErrorCode::PlayerSsoSecretNotConfigured->value,
                503,
            );
        }

        $alg = (string) config('lottery.player_auth.jwt.algorithm', 'HS256');

        try {
            /** @var object $claims */
            $claims = JWT::decode($jwt, new Key($secret, $alg));
        } catch (\Throwable) {
            throw new PlayerAuthenticationException('Token 无效或已过期', ErrorCode::PlayerTokenInvalid->value);
        }

        $this->assertNativeJwtTemporalPolicy($claims);

        $playerIdKey = (string) config('lottery.player_auth.native.claim_player_id', 'player_id');
        $authKey = (string) config('lottery.player_auth.native.claim_auth_source', 'auth_source');
        $playerId = (int) data_get($claims, $playerIdKey, 0);
        $authSource = data_get($claims, $authKey);

        if ($playerId <= 0 || $authSource !== PlayerAuthSource::LOTTERY_NATIVE) {
            throw new PlayerAuthenticationException('JWT 缺少玩家标识', ErrorCode::PlayerTokenInvalid->value);
        }

        $player = Player::query()->find($playerId);
        if ($player === null || ! $player->isLotteryNative()) {
            throw new PlayerAuthenticationException('玩家不存在', ErrorCode::PlayerNotRegistered->value);
        }

        $player->forceFill(['last_login_at' => now()])->save();

        return $player->refresh();
    }

    private function resolveSsoJwt(string $jwt, string $secret): Player
    {
        $alg = (string) config('lottery.player_auth.jwt.algorithm', 'HS256');

        try {
            /** @var object $claims */
            $claims = JWT::decode($jwt, new Key($secret, $alg));
        } catch (\Throwable $e) {
            // 签名错误、exp 过期、格式损坏等均归 8002
            throw new PlayerAuthenticationException('Token 无效或已过期', ErrorCode::PlayerTokenInvalid->value);
        }

        $this->assertJwtTemporalPolicy($claims);

        // 与主站约定 JWT 里字段名；若主站用 sub/iss 等可改 env LOTTERY_JWT_CLAIM_*
        $siteKey = (string) config('lottery.player_auth.jwt.claim_site_code', 'site_code');
        $pidKey = (string) config('lottery.player_auth.jwt.claim_site_player_id', 'site_player_id');

        $siteCode = data_get($claims, $siteKey);
        $sitePlayerId = data_get($claims, $pidKey);

        if (! is_string($siteCode) || $siteCode === '' || ! is_string($sitePlayerId) || $sitePlayerId === '') {
            throw new PlayerAuthenticationException('JWT 缺少站点或玩家标识', ErrorCode::PlayerTokenInvalid->value);
        }

        $now = now();
        $defaults = [
            ...PlayerAutoRegistrationDefaults::profileFields(),
            'auth_source' => PlayerAuthSource::MAIN_SITE_SSO,
            'funding_mode' => PlayerFundingMode::WALLET,
            'default_currency' => LotterySettings::defaultCurrency(),
            'status' => self::PLAYER_STATUS_ACTIVE,
            'last_login_at' => $now,
        ];

        try {
            $player = Player::query()->firstOrCreate(
                [
                    'site_code' => $siteCode,
                    'site_player_id' => $sitePlayerId,
                ],
                $defaults,
            );
        } catch (QueryException $e) {
            // 并发首登时可能重复插入唯一键，改为加载已有行
            $player = Player::query()
                ->where('site_code', $siteCode)
                ->where('site_player_id', $sitePlayerId)
                ->first();
            if ($player === null) {
                throw $e;
            }
        }

        if (! $player->wasRecentlyCreated) {
            $player->forceFill(['last_login_at' => $now])->save();
        }

        return $player->refresh();
    }

    private function peekAuthSourceFromJwt(string $jwt): ?string
    {
        $parts = explode('.', trim($jwt));
        if (count($parts) !== 3) {
            return null;
        }

        $payload = json_decode($this->base64UrlDecode($parts[1]), true);
        if (! is_array($payload)) {
            return null;
        }

        $authKey = (string) config('lottery.player_auth.native.claim_auth_source', 'auth_source');
        $value = $payload[$authKey] ?? null;

        return is_string($value) ? $value : null;
    }

    private function base64UrlDecode(string $segment): string
    {
        $remainder = strlen($segment) % 4;
        if ($remainder > 0) {
            $segment .= str_repeat('=', 4 - $remainder);
        }

        $decoded = base64_decode(strtr($segment, '-_', '+/'), true);

        return is_string($decoded) ? $decoded : '';
    }

    /**
     * @param  object  $claims
     */
    private function assertNativeJwtTemporalPolicy(object $claims): void
    {
        if (! isset($claims->exp) || ! is_numeric($claims->exp)) {
            throw new PlayerAuthenticationException('JWT 缺少过期时间', ErrorCode::PlayerTokenInvalid->value);
        }

        $maxTtl = (int) config('lottery.player_auth.native.ttl_seconds', 28800);
        if (isset($claims->iat) && is_numeric($claims->iat)) {
            $iat = (int) $claims->iat;
            $exp = (int) $claims->exp;
            if ($exp - $iat > $maxTtl) {
                throw new PlayerAuthenticationException(
                    'JWT 有效期超过允许的 '.(string) $maxTtl.' 秒',
                    ErrorCode::PlayerTokenInvalid->value,
                );
            }
        }
    }

    /**
     * 短效 SSO：JWT 须有 exp（由 decode 校验），可选要求 iat，且 exp-iat 不得超过配置秒数。
     *
     * @param  object  $claims  stdClass from firebase/php-jwt
     */
    private function assertJwtTemporalPolicy(object $claims): void
    {
        $maxTtl = (int) config('lottery.player_auth.jwt.max_ttl_seconds', 300);
        $requireIat = (bool) config('lottery.player_auth.jwt.require_iat_claim', true);

        if (! isset($claims->exp) || ! is_numeric($claims->exp)) {
            throw new PlayerAuthenticationException(
                'JWT 缺少过期时间',
                ErrorCode::PlayerTokenInvalid->value,
            );
        }

        $exp = (int) $claims->exp;

        if ($requireIat) {
            if (! isset($claims->iat) || ! is_numeric($claims->iat)) {
                throw new PlayerAuthenticationException(
                    'JWT 缺少签发时间',
                    ErrorCode::PlayerTokenInvalid->value,
                );
            }
            $iat = (int) $claims->iat;
            if ($exp <= $iat) {
                throw new PlayerAuthenticationException(
                    'JWT 时间与签名无效',
                    ErrorCode::PlayerTokenInvalid->value,
                );
            }
            if ($exp - $iat > $maxTtl) {
                throw new PlayerAuthenticationException(
                    'JWT 有效期超过允许的 '.(string) $maxTtl.' 秒',
                    ErrorCode::PlayerTokenInvalid->value,
                );
            }
        }
    }

    private function assertPlayerActive(Player $player): void
    {
        if ((int) $player->status !== self::PLAYER_STATUS_ACTIVE) {
            throw new PlayerAuthenticationException(
                '账号已冻结或不可用',
                ErrorCode::PlayerAccountSuspended->value,
                403,
            );
        }
    }
}
