<?php

namespace App\Services;

use App\Models\TwoFactorCode;
use App\Models\TwoFactorSession;
use App\Notifications\TwoFactorCodeNotification;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class TwoFactorAuthService
{
    /**
     * Generate and send a 2FA code to the user.
     *
     * @param  \Illuminate\Foundation\Auth\User  $user  User or PortalUser
     * @return string The session token for this 2FA attempt
     */
    public function generateAndSendCode($user): string
    {
        // Delete any existing unused codes for this user
        $user->twoFactorCodes()
            ->valid()
            ->delete();

        // Generate a random 6-digit code
        $code = $this->generateSixDigitCode();

        // Store the hashed code
        $twoFactorCode = $user->twoFactorCodes()->create([
            'code_hash' => Hash::make($code),
            'expires_at' => now()->addMinutes(10), // 10-minute expiration
            'attempts' => 0,
            'used' => false,
        ]);

        // Send the code via email
        $user->notify(new TwoFactorCodeNotification($code));

        // Create or update a 2FA session
        $sessionToken = TwoFactorSession::generateToken();

        $user->twoFactorSessions()->create([
            'session_token' => $sessionToken,
            'verified' => false,
            'expires_at' => now()->addMinutes(30), // 30-minute session
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
        ]);

        return $sessionToken;
    }

    /**
     * Verify a 2FA code.
     *
     * @param  \Illuminate\Foundation\Auth\User  $user
     * @param  string  $code
     * @param  string  $sessionToken
     * @return bool
     *
     * @throws ValidationException
     */
    public function verifyCode($user, string $code, string $sessionToken): bool
    {
        // Find the session
        $session = $user->twoFactorSessions()
            ->where('session_token', $sessionToken)
            ->valid()
            ->first();

        if (! $session) {
            throw ValidationException::withMessages([
                'session_token' => ['Invalid or expired session. Please log in again.'],
            ]);
        }

        // Find the most recent valid code
        $twoFactorCode = $user->twoFactorCodes()
            ->valid()
            ->orderBy('created_at', 'desc')
            ->first();

        if (! $twoFactorCode) {
            throw ValidationException::withMessages([
                'code' => ['No valid verification code found. Please request a new code.'],
            ]);
        }

        // Check if too many attempts
        if ($twoFactorCode->hasExceededAttempts()) {
            throw ValidationException::withMessages([
                'code' => ['Too many failed attempts. Please request a new code.'],
            ]);
        }

        // Verify the code
        if (! $twoFactorCode->verify($code)) {
            $twoFactorCode->incrementAttempts();

            $remainingAttempts = 5 - $twoFactorCode->attempts;

            throw ValidationException::withMessages([
                'code' => ["Invalid verification code. {$remainingAttempts} attempts remaining."],
            ]);
        }

        // Mark code as used and session as verified
        $twoFactorCode->markAsUsed();
        $session->markAsVerified();

        // Mark 2FA as set up if this is the first time
        if (! $user->hasCompletedTwoFactorSetup()) {
            $user->enableTwoFactor();
        }

        return true;
    }

    /**
     * Check if a session is verified.
     *
     * @param  \Illuminate\Foundation\Auth\User  $user
     * @param  string  $sessionToken
     * @return bool
     */
    public function isSessionVerified($user, string $sessionToken): bool
    {
        return $user->twoFactorSessions()
            ->where('session_token', $sessionToken)
            ->verified()
            ->valid()
            ->exists();
    }

    /**
     * Clean up expired codes and sessions.
     */
    public function cleanupExpired(): void
    {
        TwoFactorCode::where('expires_at', '<', now())->delete();
        TwoFactorSession::where('expires_at', '<', now())->delete();
    }

    /**
     * Resend a 2FA code (reuses existing session).
     *
     * @param  \Illuminate\Foundation\Auth\User  $user
     * @param  string  $sessionToken
     * @return void
     *
     * @throws ValidationException
     */
    public function resendCode($user, string $sessionToken): void
    {
        // Verify session exists and is valid
        $session = $user->twoFactorSessions()
            ->where('session_token', $sessionToken)
            ->valid()
            ->first();

        if (! $session) {
            throw ValidationException::withMessages([
                'session_token' => ['Invalid or expired session. Please log in again.'],
            ]);
        }

        // Delete existing codes
        $user->twoFactorCodes()->valid()->delete();

        // Generate new code
        $code = $this->generateSixDigitCode();

        // Store the hashed code
        $user->twoFactorCodes()->create([
            'code_hash' => Hash::make($code),
            'expires_at' => now()->addMinutes(10),
            'attempts' => 0,
            'used' => false,
        ]);

        // Send the code via email
        $user->notify(new TwoFactorCodeNotification($code));
    }

    /**
     * Reset 2FA for a user (admin action only).
     *
     * @param  \Illuminate\Foundation\Auth\User  $user
     */
    public function resetTwoFactor($user): void
    {
        // Delete all codes and sessions
        $user->twoFactorCodes()->delete();
        $user->twoFactorSessions()->delete();

        // Reset setup timestamp to force re-setup
        $user->two_factor_setup_at = null;
        $user->save();
    }

    /**
     * Generate a random 6-digit code.
     *
     * @return string
     */
    protected function generateSixDigitCode(): string
    {
        return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    }
}
