<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Concerns\WritesAuthLog;
use App\Http\Controllers\Controller;
use App\Models\UserRecoveryCode;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use PragmaRX\Google2FA\Google2FA;

/**
 * Управление 2FA: setup wizard (init+confirm), disable, regenerate-recovery-codes.
 *
 * Источник: ТЗ §1.6 / §22.4.2 / Прил. Г.4.2.
 *
 * Все эндпоинты под `auth:sanctum` — настройка 2FA доступна только
 * авторизованному user'у. Disable/regenerate требуют password-confirmation.
 *
 * Recovery-код формат — `XXXX-XXXX` (8 hex-байт через дефис), хранятся
 * как bcrypt в `user_recovery_codes`. Генерация ровно 8 кодов на user'а.
 */
class TwoFactorSetupController extends Controller
{
    use WritesAuthLog;

    private const RECOVERY_CODES_COUNT = 8;

    /**
     * POST /api/2fa/init — начало setup wizard.
     *
     * Генерирует новый TOTP secret и кладёт в session как pending. user
     * сканирует QR в приложении-аутентификаторе и переходит к confirm.
     * Secret НЕ сохраняется в БД до подтверждения первым кодом — иначе
     * user мог бы случайно «активировать» сломанный TOTP.
     */
    public function init(Request $request): JsonResponse
    {
        $user = $request->user();
        if (! $user) {
            return response()->json(['message' => 'Требуется вход.'], 401);
        }

        if ($user->totp_enabled) {
            return response()->json([
                'message' => '2FA уже включена. Сначала отключите её, чтобы начать заново.',
            ], 422);
        }

        $google2fa = new Google2FA;
        $secret = $google2fa->generateSecretKey();

        $request->session()->put('auth.pending_totp_secret', $secret);

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

        // QR-URL формата `otpauth://totp/...` — user сканирует через приложение.
        // По стандарту RFC 6238: issuer + label + secret + period.
        $qrUrl = $google2fa->getQRCodeUrl(
            'Лидерра',
            (string) $user->email,
            $secret,
        );

        return response()->json([
            'secret' => $secret, // показываем для manual-input fallback
            'qr_url' => $qrUrl,
        ]);
    }

    /**
     * POST /api/2fa/confirm — подтверждение setup первым TOTP-кодом.
     *
     * На success:
     *   1. Сохраняем pending_totp_secret в users.totp_secret (encrypted cast).
     *   2. Ставим totp_enabled = true.
     *   3. Удаляем все existing recovery_codes (если перерегистрация).
     *   4. Генерируем 8 новых recovery-кодов, возвращаем plain один раз.
     */
    public function confirm(Request $request): JsonResponse
    {
        $user = $request->user();
        if (! $user) {
            return response()->json(['message' => 'Требуется вход.'], 401);
        }

        $pendingSecret = $request->session()->get('auth.pending_totp_secret');
        if (! $pendingSecret) {
            return response()->json([
                'message' => 'Не начат setup wizard. Запустите заново.',
            ], 422);
        }

        $code = $request->string('code')->toString();
        if (! preg_match('/^\d{6}$/', $code)) {
            return response()->json([
                'message' => 'Неверный формат кода.',
                'errors' => ['code' => ['Введите 6 цифр.']],
            ], 422);
        }

        $google2fa = new Google2FA;
        if (! $google2fa->verifyKey($pendingSecret, $code, 1)) {
            return response()->json([
                'message' => 'Неверный код. Проверьте часы устройства и попробуйте снова.',
                'errors' => ['code' => ['Неверный код.']],
            ], 422);
        }

        // Активируем 2FA + перегенерируем recovery codes (атомарно).
        $plainCodes = DB::transaction(function () use ($user, $pendingSecret) {
            $user->forceFill([
                'totp_secret' => $pendingSecret,
                'totp_enabled' => true,
            ])->save();

            UserRecoveryCode::query()->where('user_id', $user->id)->delete();

            return $this->generateRecoveryCodes($user->id);
        });

        $request->session()->forget('auth.pending_totp_secret');

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

        return response()->json([
            'recovery_codes' => $plainCodes,
            'message' => '2FA включена. Сохраните резервные коды — они показываются один раз.',
        ]);
    }

    /**
     * POST /api/2fa/disable — отключение 2FA (требует пароль).
     */
    public function disable(Request $request): JsonResponse
    {
        $user = $request->user();
        if (! $user) {
            return response()->json(['message' => 'Требуется вход.'], 401);
        }

        $password = $request->string('password')->toString();
        if ($password === '' || ! Hash::check($password, $user->password_hash)) {
            $this->logAuthEvent('2fa_disable_failed', $user->id, $user->tenant_id, $user->email,
                $request->ip(), $request->userAgent(), 'invalid_password');

            return response()->json([
                'message' => 'Неверный пароль.',
                'errors' => ['password' => ['Неверный пароль.']],
            ], 422);
        }

        DB::transaction(function () use ($user) {
            $user->forceFill([
                'totp_secret' => null,
                'totp_enabled' => false,
            ])->save();

            UserRecoveryCode::query()->where('user_id', $user->id)->delete();
        });

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

        return response()->json(['message' => '2FA отключена.']);
    }

    /**
     * POST /api/2fa/regenerate-recovery-codes — перегенерация 8 кодов (требует пароль).
     */
    public function regenerateRecoveryCodes(Request $request): JsonResponse
    {
        $user = $request->user();
        if (! $user) {
            return response()->json(['message' => 'Требуется вход.'], 401);
        }

        if (! $user->totp_enabled) {
            return response()->json([
                'message' => '2FA не включена.',
            ], 422);
        }

        $password = $request->string('password')->toString();
        if ($password === '' || ! Hash::check($password, $user->password_hash)) {
            return response()->json([
                'message' => 'Неверный пароль.',
                'errors' => ['password' => ['Неверный пароль.']],
            ], 422);
        }

        $plainCodes = DB::transaction(function () use ($user) {
            UserRecoveryCode::query()->where('user_id', $user->id)->delete();

            return $this->generateRecoveryCodes($user->id);
        });

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

        return response()->json([
            'recovery_codes' => $plainCodes,
            'message' => 'Резервные коды перегенерированы.',
        ]);
    }

    /**
     * Генерирует 8 plain-кодов формата XXXX-XXXX и сохраняет их bcrypt-хеши.
     *
     * @return array<int, string>
     */
    private function generateRecoveryCodes(int $userId): array
    {
        $plainCodes = [];
        $rows = [];
        for ($i = 0; $i < self::RECOVERY_CODES_COUNT; $i++) {
            // Str::random возвращает [a-zA-Z0-9], но recovery-format требует
            // только [a-z0-9] (нормализация в /recovery-use делает strtolower).
            $part1 = mb_strtolower(Str::random(4));
            $part2 = mb_strtolower(Str::random(4));
            $plain = $part1.'-'.$part2;
            $plainCodes[] = $plain;

            // Hash::check сравнивает по нормализованному коду (без дефиса).
            $rows[] = [
                'user_id' => $userId,
                'code_hash' => Hash::make($part1.$part2),
                'used_at' => null,
                'created_at' => now(),
            ];
        }
        DB::table('user_recovery_codes')->insert($rows);

        return $plainCodes;
    }
}
