<?php

namespace App\Services\Player;

use App\Lottery\ErrorCode;
use App\Models\Player;
use App\Support\PlayerAuthSource;
use Firebase\JWT\JWT;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use App\Exceptions\PlayerAuthenticationException;

final class PlayerNativeAuthService
{
    /**
     * @return array{access_token: string, expires_in: int, token_type: string, player: array<string, mixed>}
     */
    public function login(string $siteCode, string $username, string $password): array
    {
        $username = trim($username);
        $siteCode = trim($siteCode);
        if ($username === '' || $password === '') {
            throw new PlayerAuthenticationException(
                '账号或密码错误',
                ErrorCode::PlayerCredentialsInvalid->value,
            );
        }

        $player = $this->resolveNativePlayer($siteCode, $username);

        if ($player === null || ! is_string($player->password_hash) || $player->password_hash === '') {
            throw new PlayerAuthenticationException(
                '账号或密码错误',
                ErrorCode::PlayerCredentialsInvalid->value,
            );
        }

        if ($player->login_locked_until !== null && $player->login_locked_until->isFuture()) {
            throw new PlayerAuthenticationException(
                '登录已锁定',
                ErrorCode::PlayerLoginLocked->value,
                403,
            );
        }

        if ((int) $player->status !== 0) {
            throw new PlayerAuthenticationException(
                '账号已冻结',
                ErrorCode::PlayerAccountSuspended->value,
                403,
            );
        }

        if (! Hash::check($password, $player->password_hash)) {
            $this->recordFailedLogin($player);

            throw new PlayerAuthenticationException(
                '账号或密码错误',
                ErrorCode::PlayerCredentialsInvalid->value,
            );
        }

        $player->forceFill([
            'login_failed_count' => 0,
            'login_locked_until' => null,
            'last_login_at' => now(),
        ])->save();

        $ttl = (int) config('lottery.player_auth.native.ttl_seconds', 28800);
        $token = $this->issueToken($player, $ttl);

        return [
            'access_token' => $token,
            'expires_in' => $ttl,
            'token_type' => 'Bearer',
            'player' => [
                'id' => (int) $player->id,
                'site_code' => $player->site_code,
                'username' => $player->username,
                'nickname' => $player->nickname,
                'funding_mode' => $player->funding_mode,
                'auth_source' => $player->auth_source,
            ],
        ];
    }

    public function issueToken(Player $player, ?int $ttlSeconds = null): string
    {
        $secret = (string) config('lottery.player_auth.native.secret', '');
        if ($secret === '') {
            throw new PlayerAuthenticationException(
                '原生登录未配置',
                ErrorCode::PlayerSsoSecretNotConfigured->value,
                503,
            );
        }

        $ttl = $ttlSeconds ?? (int) config('lottery.player_auth.native.ttl_seconds', 28800);
        $now = time();
        $playerIdKey = (string) config('lottery.player_auth.native.claim_player_id', 'player_id');
        $authKey = (string) config('lottery.player_auth.native.claim_auth_source', 'auth_source');

        $payload = [
            $playerIdKey => (int) $player->id,
            $authKey => PlayerAuthSource::LOTTERY_NATIVE,
            'site_code' => (string) $player->site_code,
            'iat' => $now,
            'exp' => $now + $ttl,
        ];

        return JWT::encode($payload, $secret, (string) config('lottery.player_auth.jwt.algorithm', 'HS256'));
    }

    private function recordFailedLogin(Player $player): void
    {
        $max = (int) config('lottery.player_auth.native.max_login_attempts', 8);
        $lockMinutes = (int) config('lottery.player_auth.native.lock_minutes', 15);

        // 并发场景下两个错误登录请求都拿到旧 login_failed_count 各自 +1 时，会出现 last-write-wins
        // 丢失一次失败计数（影响锁定判定准确性）。用 lockForUpdate 取最新计数后再写。
        \DB::transaction(function () use ($player, $max, $lockMinutes): void {
            $latest = Player::query()
                ->whereKey((int) $player->id)
                ->lockForUpdate()
                ->first();

            if ($latest === null) {
                return;
            }

            $count = (int) $latest->login_failed_count + 1;

            $updates = ['login_failed_count' => $count];
            if ($count >= $max) {
                $updates['login_locked_until'] = now()->addMinutes($lockMinutes);
                $updates['login_failed_count'] = 0;
            }

            Player::query()
                ->whereKey((int) $player->id)
                ->update($updates);
        });
    }

    /**
     * 彩票端登录：玩家只填账号密码，不传站点编号。
     * 若请求带了 site_code（部署绑站）则优先在该站查找；否则按账号全局匹配唯一彩票原生玩家。
     */
    private function resolveNativePlayer(string $siteCode, string $username): ?Player
    {
        $query = Player::query()
            ->where('username', $username)
            ->where('auth_source', PlayerAuthSource::LOTTERY_NATIVE);

        if ($siteCode !== '') {
            $scoped = (clone $query)->where('site_code', $siteCode)->first();
            if ($scoped !== null) {
                return $scoped;
            }
        }

        $candidates = $query->get();
        if ($candidates->count() === 1) {
            return $candidates->first();
        }

        return null;
    }
}
