<?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\LoginRequest;
use App\Mail\SuspiciousLoginNotification;
use App\Models\ImpersonationToken;
use App\Models\Tenant;
use App\Models\User;
use App\Services\NotificationService;
use App\Services\UserSessionTracker;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;

/**
 * Аутентификация через Sanctum SPA mode (cookie-based session).
 * Core auth flow: login / register / logout / me + notification preferences.
 *
 * Sprint 3 Phase B (audit O-refactor-02): 2FA verify/recovery вынесены в
 * TwoFactorController, password reset — в PasswordResetController.
 * Setup 2FA — TwoFactorSetupController (был отдельным ещё с Sprint 1).
 *
 * Flow без 2FA:
 *   1. POST /api/auth/login → создаёт session, возвращает {user, requires_2fa: false}.
 *
 * Flow с 2FA (totp_enabled=true):
 *   1. POST /api/auth/login → проверяет email+password, сохраняет pending_user_id
 *      в session, **НЕ** делает Auth::login. Возвращает {user, requires_2fa: true}.
 *   2. POST /api/auth/2fa/verify {code} → TwoFactorController, проверяет TOTP
 *      против users.totp_secret, окно ±1 (30 сек до/после). Если ок — Auth::login.
 *
 * Rate-limit (ТЗ §22.4.4):
 * - Login: 5 попыток / 15 мин по ключу email|ip. Превышение → 429 + Retry-After.
 * - IP-lockout: 10 неудач/час с одного IP через auth_log → 429 + Retry-After: 3600.
 * - Email-уведомление при ровно 3 неудачах входа на email → SuspiciousLoginNotification.
 *
 * Не входит:
 * - Yandex 360 SSO.
 * - Tenant-wizard при register (на MVP — first tenant attach).
 */
class AuthController 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;

    /** Лимит неудач входа с одного IP за час (ТЗ §22.4.4 п.2). */
    private const IP_LOCKOUT_THRESHOLD = 10;

    public function login(LoginRequest $request): JsonResponse
    {
        $credentials = $request->only(['email', 'password']);
        $throttleKey = $this->loginThrottleKey($credentials['email'], $request->ip());
        $ip = $request->ip();

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

        // ТЗ §22.4.4 п.2: IP-lockout — 10 неудач/час с одного IP → блок IP на 1 час.
        if ($this->isIpLockedOut($ip)) {
            return response()->json([
                'message' => 'Слишком много неудачных попыток с этого IP. Попробуйте через час.',
                'retry_after' => 3600,
            ], 429)->header('Retry-After', '3600');
        }

        $user = User::where('email', $credentials['email'])->first();

        if (! $user || ! Hash::check($credentials['password'], $user->password_hash)) {
            RateLimiter::hit($throttleKey, self::LOGIN_DECAY_SECONDS);
            $this->logAuthEvent('login_failed', $user?->id, $user?->tenant_id, $credentials['email'], $ip, $request->userAgent(),
                $user ? 'invalid_password' : 'unknown_email');
            $this->maybeNotifySuspiciousLogin($user, $ip);

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

        if (! $user->is_active) {
            RateLimiter::hit($throttleKey, self::LOGIN_DECAY_SECONDS);

            // Косяк 05: неподтверждённая почта — это НЕ блокировка. Новый клиент
            // создаётся is_active=false до ввода кода из письма. Не пугаем
            // «Аккаунт заблокирован», а зовём подтвердить почту.
            if ($user->email_verified_at === null) {
                $this->logAuthEvent('login_failed', $user->id, $user->tenant_id, $credentials['email'], $ip, $request->userAgent(),
                    'email_not_confirmed');

                $msg = 'Подтвердите почту — мы отправили код на '.$user->email.'.';

                return response()->json([
                    'message' => $msg,
                    'errors' => ['email' => [$msg]],
                    'email_not_confirmed' => true,
                ], 422);
            }

            $this->logAuthEvent('login_failed', $user->id, $user->tenant_id, $credentials['email'], $ip, $request->userAgent(),
                'account_locked');

            return response()->json([
                'message' => 'Аккаунт заблокирован.',
                'errors' => ['email' => ['Аккаунт заблокирован.']],
            ], 422);
        }

        // Email+пароль приняты → счётчик чистим, чтобы 2FA-фаза не зависела от login-fails.
        RateLimiter::clear($throttleKey);

        // Если включена 2FA — НЕ делаем Auth::login сразу. Сохраняем
        // pending_user_id в session, ждём POST /api/auth/2fa/verify.
        if ($user->totp_enabled) {
            $request->session()->put('auth.pending_user_id', $user->id);
            $request->session()->put('auth.pending_remember', $request->boolean('remember', false));

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

        $remember = $request->boolean('remember', false);
        Auth::login($user, $remember);
        $request->session()->regenerate();

        $user->update(['last_login_at' => now()]);

        $this->logAuthEvent('login_success', $user->id, $user->tenant_id, $user->email, $ip, $request->userAgent(), null);
        app(UserSessionTracker::class)->record($request, $user->id);

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

    public function me(Request $request): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();
        $resource = $this->userResource($user);

        $marker = $request->hasSession() ? $request->session()->get('impersonation') : null;
        if ($marker !== null) {
            $token = ImpersonationToken::on('pgsql_supplier')->find($marker['token_id']);
            $tenant = $token?->tenant;
            $resource['impersonation'] = [
                'active' => true,
                'tenant_name' => $tenant?->organization_name,
                'started_at' => $marker['started_at'] ?? null,
                'expires_at' => $token?->sessionExpiresAt()?->toIso8601String(),
            ];
        }

        return response()->json(['user' => $resource]);
    }

    public function logout(Request $request): JsonResponse
    {
        $userId = $request->user()?->id;
        $tenantId = $request->user()?->tenant_id;
        $email = $request->user()?->email;

        // Снять текущую сессию из списка «Активные» до инвалидации (id ещё прежний).
        app(UserSessionTracker::class)->revokeCurrent($request);

        Auth::guard('web')->logout();

        $request->session()->invalidate();
        $request->session()->regenerateToken();

        $this->logAuthEvent('logout', $userId, $tenantId, $email, $request->ip(), $request->userAgent(), null);

        return response()->json(['message' => 'Вы вышли из системы.']);
    }

    /**
     * PATCH /api/auth/me/notification-preferences — сохранить матрицу
     * 8 событий × 3 каналов (inapp/push/email) + sound_enabled.
     *
     * Источник: schema.sql:699 users.notification_preferences JSONB DEFAULT.
     * Валидация: события ∈ NotificationService::ALL_EVENTS, каналы ∈
     * {inapp, push, email}. Незадекларированные ключи отбрасываются.
     */
    public function updateNotificationPreferences(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'prefs' => 'required|array',
            'prefs.*' => 'array',
            'sound_enabled' => 'nullable|boolean',
        ]);

        $allEvents = NotificationService::ALL_EVENTS;
        $allChannels = [
            NotificationService::CHANNEL_INAPP,
            NotificationService::CHANNEL_PUSH,
            NotificationService::CHANNEL_EMAIL,
        ];

        // Очищенная матрица (только known events × known channels).
        $sanitized = [];
        foreach ($validated['prefs'] as $event => $channelPrefs) {
            if (! in_array($event, $allEvents, true)) {
                continue;
            }
            $sanitized[$event] = [];
            foreach ($allChannels as $channel) {
                if (isset($channelPrefs[$channel])) {
                    $sanitized[$event][$channel] = (bool) $channelPrefs[$channel];
                }
            }
        }

        /** @var User $user */
        $user = $request->user();
        $update = ['notification_preferences' => $sanitized];
        if (array_key_exists('sound_enabled', $validated)) {
            $update['sound_enabled'] = (bool) $validated['sound_enabled'];
        }
        $user->update($update);

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

    /**
     * PATCH /api/auth/me — обновление профиля текущего пользователя
     * (имя, фамилия, телефон, тайм-зона). Email менять нельзя (через support).
     *
     * Audit J6/D1 (ProfileTab). Зеркалит updateNotificationPreferences:
     * та же группа auth:sanctum, тот же inline-validate, тот же userResource.
     */
    public function updateProfile(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'first_name' => ['required', 'string', 'max:255'],
            'last_name' => ['required', 'string', 'max:255'],
            'phone' => ['nullable', 'string', 'max:20'],
            'timezone' => ['required', 'timezone'],
        ]);

        /** @var User $user */
        $user = $request->user();
        $user->update($validated);

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

    /**
     * Ключ throttle для login: email|ip — защищает email от брутфорса даже
     * за NAT'ом, и IP от перебора емейлов с одного источника.
     */
    private function loginThrottleKey(string $email, ?string $ip): string
    {
        return 'auth:login:'.mb_strtolower($email).'|'.($ip ?? 'unknown');
    }

    /**
     * IP-lockout по ТЗ §22.4.4 п.2: 10+ login_failed с одного IP за последний час.
     *
     * Считаем через `auth_log` (event=login_failed) — это даёт защиту от
     * перебора email'ов с одного IP даже если каждый email используется
     * <5 раз и не триггерит email-lockout.
     */
    private function isIpLockedOut(?string $ip): bool
    {
        if ($ip === null || $ip === '') {
            return false;
        }

        $count = DB::table('auth_log')
            ->where('event', 'login_failed')
            ->where('ip_address', $ip)
            ->where('created_at', '>=', now()->subHour())
            ->count();

        return $count >= self::IP_LOCKOUT_THRESHOLD;
    }

    /**
     * ТЗ §22.4.4 п.3: при превышении 3 неудачных попыток входа на email
     * пользователю отправляется email-уведомление о подозрительной активности.
     *
     * Триггерим РОВНО на 3-й неудаче (не на каждой) — иначе спам уведомлений.
     * Для unknown email user=null → ничего не отправляем (некому).
     *
     * NB: считаем уже после INSERT'а login_failed события — поэтому именно
     * 3 (не 2): на 3-й попытке count = 3.
     */
    private function maybeNotifySuspiciousLogin(?User $user, ?string $ip): void
    {
        if ($user === null) {
            return;
        }

        $count = DB::table('auth_log')
            ->where('event', 'login_failed')
            ->where('user_id', $user->id)
            ->where('created_at', '>=', now()->subHour())
            ->count();

        if ($count === 3) {
            Mail::to($user->email)->send(new SuspiciousLoginNotification($user, $ip, $count));
        }
    }

    /** 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,
            'phone' => $user->phone,
            'timezone' => $user->timezone,
            '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,
        ];
    }
}
