<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Concerns\WritesAuthLog;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\UseRecoveryCodeRequest;
use App\Http\Requests\Auth\VerifyTwoFactorRequest;
use App\Models\User;
use App\Models\UserRecoveryCode;
use App\Services\UserSessionTracker;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use PragmaRX\Google2FA\Google2FA;

/**
 * 2FA login-flow — TOTP verify + recovery code use.
 * Извлечено из AuthController (Sprint 3 Phase B, audit O-refactor-02).
 *
 * Setup / enable / disable / regenerate-recovery-codes — в TwoFactorSetupController
 * (отдельный класс, route prefix /api/2fa, требует auth:sanctum).
 *
 * Эти endpoint'ы (verify, recovery-use) — публичные (нет полноценной session-auth
 * до verify): user проходит email+password в AuthController::login, который
 * сохраняет pending_user_id в session, затем сюда — для завершения login.
 *
 * Rate-limit (ТЗ §22.4.4):
 * - 2FA verify: 5 попыток / 15 мин по pending_user_id|ip.
 * - Recovery use: 5 попыток / 15 мин по pending_user_id|ip (отдельный scope).
 */
class TwoFactorController extends Controller
{
    use WritesAuthLog;

    /** Лимит попыток в окне (ТЗ §22.4.4 + system_settings.login_max_attempts=5). */
    private const LOGIN_MAX_ATTEMPTS = 5;

    /** Окно блокировки в секундах (ТЗ §22.4.4: 15 мин). */
    private const LOGIN_DECAY_SECONDS = 900;

    public function verifyTwoFactor(VerifyTwoFactorRequest $request): JsonResponse
    {
        $pendingUserId = $request->session()->get('auth.pending_user_id');
        $remember = (bool) $request->session()->get('auth.pending_remember', false);

        if (! $pendingUserId) {
            return response()->json([
                'message' => 'Сессия 2FA истекла. Войдите снова.',
            ], 422);
        }

        $throttleKey = $this->twoFactorThrottleKey((int) $pendingUserId, $request->ip());

        if (RateLimiter::tooManyAttempts($throttleKey, self::LOGIN_MAX_ATTEMPTS)) {
            return $this->lockoutResponse($throttleKey);
        }

        $user = User::find($pendingUserId);
        if (! $user || ! $user->totp_enabled || empty($user->totp_secret)) {
            return response()->json([
                'message' => 'Сессия 2FA недействительна.',
            ], 422);
        }

        $google2fa = new Google2FA;
        // Окно ±1 (30 сек до/после) — компенсирует clock-skew между сервером и
        // приложением аутентификатора. По ТЗ §1.6 / Прил. Г.4.2.
        $valid = $google2fa->verifyKey((string) $user->totp_secret, $request->string('code')->toString(), 1);

        if (! $valid) {
            RateLimiter::hit($throttleKey, self::LOGIN_DECAY_SECONDS);

            $this->logAuthEvent(
                '2fa_verify_failed',
                $user->id,
                $user->tenant_id,
                $user->email,
                $request->ip(),
                $request->userAgent(),
                'invalid_code',
            );

            return response()->json([
                'message' => 'Неверный код. Проверьте время на устройстве и попробуйте снова.',
                'errors' => ['code' => ['Неверный код.']],
            ], 422);
        }

        // 2FA пройдена → чистим throttle + залогиниваем + чистим pending session.
        RateLimiter::clear($throttleKey);

        Auth::login($user, $remember);
        $request->session()->regenerate();
        $request->session()->forget(['auth.pending_user_id', 'auth.pending_remember']);

        $user->update(['last_login_at' => now()]);
        app(UserSessionTracker::class)->record($request, $user->id);

        $this->logAuthEvent(
            '2fa_verify_success',
            $user->id,
            $user->tenant_id,
            $user->email,
            $request->ip(),
            $request->userAgent(),
            null,
        );

        return response()->json([
            'user' => $this->userResource($user),
            'requires_2fa' => false,
        ]);
    }

    /**
     * POST /api/auth/2fa/recovery-use — вход по резервному коду вместо TOTP.
     *
     * Flow (продолжение login → pending_user_id в session):
     *   1. Из session берём pending_user_id (тот же, что для /2fa/verify).
     *   2. Перебираем все НЕиспользованные user_recovery_codes этого user'а,
     *      для каждого делаем `Hash::check($plainCode, $codeHash)`.
     *   3. На совпадении → mark used_at + Auth::login + clear pending.
     *   4. Иначе → 422 + RateLimiter::hit.
     *
     * Rate-limit: 5/15мин по pending_user_id|ip (тот же ключ, что 2fa/verify
     * был бы избыточен — но физически разные scope'ы).
     *
     * Recovery code хранится в bcrypt-хеше (security: даже при утечке БД
     * нельзя восстановить plain-код), сравнение через Hash::check.
     */
    public function useRecoveryCode(UseRecoveryCodeRequest $request): JsonResponse
    {
        $pendingUserId = $request->session()->get('auth.pending_user_id');
        $remember = (bool) $request->session()->get('auth.pending_remember', false);

        if (! $pendingUserId) {
            return response()->json([
                'message' => 'Сессия 2FA истекла. Войдите снова.',
            ], 422);
        }

        $throttleKey = 'auth:recovery:'.$pendingUserId.'|'.($request->ip() ?? 'unknown');

        if (RateLimiter::tooManyAttempts($throttleKey, self::LOGIN_MAX_ATTEMPTS)) {
            return $this->lockoutResponse($throttleKey);
        }

        $user = User::find($pendingUserId);
        if (! $user) {
            return response()->json([
                'message' => 'Сессия 2FA недействительна.',
            ], 422);
        }

        // Нормализуем: убираем дефисы и пробелы, lower-case (генерация в setup
        // wizard'е тоже нормализует — единый формат сравнения).
        $plainCode = mb_strtolower(preg_replace('/[\s\-]+/', '', $request->string('code')->toString()) ?? '');

        $unusedCodes = UserRecoveryCode::query()
            ->where('user_id', $user->id)
            ->whereNull('used_at')
            ->get();

        $matched = null;
        foreach ($unusedCodes as $row) {
            if (Hash::check($plainCode, $row->code_hash)) {
                $matched = $row;
                break;
            }
        }

        if (! $matched) {
            RateLimiter::hit($throttleKey, self::LOGIN_DECAY_SECONDS);

            $this->logAuthEvent(
                '2fa_recovery_failed',
                $user->id,
                $user->tenant_id,
                $user->email,
                $request->ip(),
                $request->userAgent(),
                'invalid_or_used',
            );

            return response()->json([
                'message' => 'Резервный код недействителен или уже использован.',
                'errors' => ['code' => ['Резервный код недействителен или уже использован.']],
            ], 422);
        }

        // Пометить код использованным + завершить login.
        $matched->update(['used_at' => now()]);

        RateLimiter::clear($throttleKey);

        Auth::login($user, $remember);
        $request->session()->regenerate();
        $request->session()->forget(['auth.pending_user_id', 'auth.pending_remember']);

        $user->update(['last_login_at' => now()]);
        app(UserSessionTracker::class)->record($request, $user->id);

        $this->logAuthEvent(
            '2fa_recovery_used',
            $user->id,
            $user->tenant_id,
            $user->email,
            $request->ip(),
            $request->userAgent(),
            null,
        );

        // Кол-во оставшихся неиспользованных кодов — для UI-warning'а
        // ("осталось 3 из 8 — рекомендуем перегенерировать").
        $remaining = UserRecoveryCode::query()
            ->where('user_id', $user->id)
            ->whereNull('used_at')
            ->count();

        return response()->json([
            'user' => $this->userResource($user),
            'requires_2fa' => false,
            'recovery_codes_remaining' => $remaining,
        ]);
    }

    /** Ключ throttle для 2FA verify: pending user_id + IP. */
    private function twoFactorThrottleKey(int $userId, ?string $ip): string
    {
        return 'auth:2fa:'.$userId.'|'.($ip ?? 'unknown');
    }

    /** 429 Too Many Requests + Retry-After header (секунды до следующей попытки). */
    private function lockoutResponse(string $throttleKey): JsonResponse
    {
        $retryAfter = RateLimiter::availableIn($throttleKey);

        return response()->json([
            'message' => "Слишком много попыток. Попробуйте через {$retryAfter} сек.",
            'retry_after' => $retryAfter,
        ], 429)->header('Retry-After', (string) $retryAfter);
    }

    /** @return array<string, mixed> */
    private function userResource(User $user): array
    {
        return [
            'id' => $user->id,
            'email' => $user->email,
            'first_name' => $user->first_name,
            'last_name' => $user->last_name,
            'tenant_id' => $user->tenant_id,
            'totp_enabled' => $user->totp_enabled,
            'last_login_at' => $user->last_login_at,
            'notification_preferences' => $user->notification_preferences,
            'sound_enabled' => $user->sound_enabled,
        ];
    }
}
