<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Models\GeneralSetting;
use App\Models\LoginOtpChallenge;
use App\Models\User;
use App\Services\CaptchaService;
use App\Services\EmailService;
use App\Services\SmsService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;

class AuthenticatedSessionController extends Controller
{
    public function create(): View
    {
        return view('auth.login', [
            'panel' => 'admin',
            'formAction' => route('login'),
            'switchUrl' => null,
            'switchLabel' => null,
        ]);
    }

    public function createEmployee(): View
    {
        return view('auth.login', [
            'panel' => 'employee',
            'formAction' => route('employee.login.store'),
            'switchUrl' => null,
            'switchLabel' => null,
        ]);
    }

    public function store(LoginRequest $request): RedirectResponse
    {
        return $this->handleLogin($request, 'login');
    }

    public function storeEmployee(LoginRequest $request): RedirectResponse
    {
        return $this->handleLogin($request, 'employee.login');
    }

    private function handleLogin(LoginRequest $request, string $entryRoute): RedirectResponse
    {
        app(CaptchaService::class)->validateOrThrow(
            $request,
            $entryRoute === 'employee.login' ? 'employee_login' : 'admin_login'
        );

        $request->ensureIsNotRateLimited();

        $credentials = $request->only('email', 'password');
        if (! Auth::validate($credentials)) {
            RateLimiter::hit($request->throttleKey());

            throw ValidationException::withMessages([
                'email' => trans('auth.failed'),
            ]);
        }

        $user = User::query()->where('email', $request->string('email')->toString())->first();
        if (! $user || ! $user->is_active) {
            RateLimiter::hit($request->throttleKey());

            throw ValidationException::withMessages([
                'email' => trans('auth.failed'),
            ]);
        }

        RateLimiter::clear($request->throttleKey());

        $security = $this->securitySettings();
        if ($this->requiresOtp($user, $security)) {
            $challenge = $this->issueOtpChallenge($request, $user, $request->boolean('remember'), $security);

            $request->session()->put('auth.pending_otp_challenge_id', $challenge->id);
            $request->session()->put('auth.pending_otp_user_id', $user->id);
            $request->session()->put('auth.login_entry_route', $entryRoute);

            return redirect()->route('login.otp.challenge')
                ->with('status', 'Verification code sent. Please confirm OTP to continue.');
        }

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

        return $this->redirectAfterLogin($request, $user, $entryRoute);
    }

    public function challenge(Request $request): RedirectResponse|View
    {
        $challenge = $this->pendingChallenge($request);
        if (! $challenge) {
            return redirect()->route($this->loginEntryRoute($request));
        }

        if (! $challenge->isAvailable()) {
            $this->clearPendingAuth($request);

            return redirect()->route($this->loginEntryRoute($request))->withErrors([
                'email' => 'OTP challenge expired. Please login again.',
            ]);
        }

        return view('auth.otp-challenge', [
            'maskedTarget' => $this->maskTarget((string) $challenge->target, (string) $challenge->channel),
            'channel' => $challenge->channel,
            'expiresAt' => $challenge->expires_at,
            'remainingAttempts' => max(0, $challenge->max_attempts - $challenge->attempts),
            'loginRoute' => $this->loginEntryRoute($request),
        ]);
    }

    public function verifyChallenge(Request $request): RedirectResponse
    {
        $request->validate([
            'otp' => ['required', 'regex:/^\d{6}$/'],
        ]);

        $challenge = $this->pendingChallenge($request);
        if (! $challenge) {
            return redirect()->route($this->loginEntryRoute($request));
        }

        if (! $challenge->isAvailable()) {
            $this->clearPendingAuth($request);

            return redirect()->route($this->loginEntryRoute($request))->withErrors([
                'email' => 'OTP challenge expired. Please login again.',
            ]);
        }

        $challenge->attempts++;
        $isValid = hash_equals($challenge->code_hash, $this->otpHash($request->string('otp')->toString()));

        if (! $isValid) {
            if ($challenge->attempts >= $challenge->max_attempts) {
                $challenge->consumed_at = now();
            }
            $challenge->save();

            if ($challenge->attempts >= $challenge->max_attempts) {
                $this->clearPendingAuth($request);

                return redirect()->route($this->loginEntryRoute($request))->withErrors([
                    'email' => 'Too many invalid OTP attempts. Please login again.',
                ]);
            }

            return back()->withErrors([
                'otp' => 'Invalid OTP code.',
            ]);
        }

        $challenge->consumed_at = now();
        $challenge->save();

        $user = $challenge->user;
        if (! $user || ! $user->is_active) {
            $this->clearPendingAuth($request);

            return redirect()->route($this->loginEntryRoute($request))->withErrors([
                'email' => trans('auth.failed'),
            ]);
        }

        $entryRoute = $this->loginEntryRoute($request);
        $this->clearPendingAuth($request);

        Auth::login($user, (bool) $challenge->remember);
        $request->session()->regenerate();

        return $this->redirectAfterLogin($request, $user, $entryRoute);
    }

    public function resendChallenge(Request $request): RedirectResponse
    {
        $challenge = $this->pendingChallenge($request);
        if (! $challenge) {
            return redirect()->route($this->loginEntryRoute($request));
        }

        if (! $challenge->isAvailable()) {
            $this->clearPendingAuth($request);

            return redirect()->route($this->loginEntryRoute($request))->withErrors([
                'email' => 'OTP challenge expired. Please login again.',
            ]);
        }

        $security = $this->securitySettings();
        $cooldown = (int) ($security['login_otp_resend_cooldown_seconds'] ?? 30);
        if ($challenge->last_sent_at && now()->diffInSeconds($challenge->last_sent_at) < $cooldown) {
            $secondsLeft = $cooldown - now()->diffInSeconds($challenge->last_sent_at);
            return back()->withErrors([
                'otp' => "Please wait {$secondsLeft} seconds before resending OTP.",
            ]);
        }

        $otp = $this->generateOtp();
        $challenge->code_hash = $this->otpHash($otp);
        $challenge->attempts = 0;
        $challenge->expires_at = now()->addMinutes((int) ($security['login_otp_expiry_minutes'] ?? 5));
        $challenge->last_sent_at = now();
        $challenge->save();

        $sent = $this->dispatchOtp((string) $challenge->channel, $challenge->user, $otp);
        if (! $sent) {
            return back()->withErrors([
                'otp' => 'Could not send OTP right now. Please try again.',
            ]);
        }

        return back()->with('status', 'A new OTP has been sent.');
    }

    public function destroy(Request $request): RedirectResponse
    {
        Auth::guard('web')->logout();
        $this->clearPendingAuth($request);

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

        return redirect('/');
    }

    private function redirectAfterLogin(Request $request, User $user, string $entryRoute = 'login'): RedirectResponse
    {
        $intendedUrl = $this->validatedIntendedUrl($request, $user);
        if ($intendedUrl) {
            $request->session()->forget('url.intended');

            return redirect()->to($intendedUrl);
        }

        if ($user->isEmployee()) {
            return redirect()->route('employee.dashboard');
        }

        return redirect()->route('admin.dashboard');
    }

    private function validatedIntendedUrl(Request $request, User $user): ?string
    {
        $intended = (string) $request->session()->get('url.intended', '');
        if ($intended === '') {
            return null;
        }

        $appUrl = rtrim((string) config('app.url'), '/');
        if ($appUrl !== '' && ! Str::startsWith($intended, [$appUrl . '/', $appUrl])) {
            return null;
        }

        $path = '/' . ltrim((string) parse_url($intended, PHP_URL_PATH), '/');
        $basePath = '/' . trim((string) parse_url((string) config('app.url'), PHP_URL_PATH), '/');
        if ($basePath !== '/' && Str::startsWith($path, $basePath)) {
            $path = '/' . ltrim(substr($path, strlen($basePath)), '/');
        }

        $adminPrefix = '/' . trim((string) env('ADMIN_PATH', 'admin'), '/');
        $employeePrefix = '/employee';
        $sharedAllowedPaths = ['/dashboard', '/notifications', '/profile'];

        if ($user->isEmployee()) {
            return Str::startsWith($path, $employeePrefix) || in_array($path, $sharedAllowedPaths, true)
                ? $intended
                : null;
        }

        if (Str::startsWith($path, $adminPrefix) || in_array($path, $sharedAllowedPaths, true)) {
            return $intended;
        }

        return null;
    }

    private function requiresOtp(User $user, array $security): bool
    {
        if ($user->isEmployee()) {
            return (bool) ($security['employee_login_otp_enabled'] ?? true);
        }

        return $user->isAdmin() || $user->isOwner()
            ? (bool) ($security['admin_login_otp_enabled'] ?? true)
            : false;
    }

    private function issueOtpChallenge(Request $request, User $user, bool $remember, array $security): LoginOtpChallenge
    {
        $channel = (string) ($security['login_otp_channel'] ?? 'email');
        $target = $channel === 'sms' ? (string) ($user->phone ?? '') : (string) ($user->email ?? '');

        if ($target === '') {
            $message = $channel === 'sms'
                ? 'SMS OTP is enabled but this account has no phone number.'
                : 'Email OTP could not be sent because this account has no email.';

            throw ValidationException::withMessages(['email' => $message]);
        }

        LoginOtpChallenge::query()
            ->where('user_id', $user->id)
            ->whereNull('consumed_at')
            ->update(['consumed_at' => now()]);

        $otp = $this->generateOtp();
        $challenge = LoginOtpChallenge::create([
            'user_id' => $user->id,
            'code_hash' => $this->otpHash($otp),
            'channel' => $channel,
            'target' => $target,
            'attempts' => 0,
            'max_attempts' => (int) ($security['login_otp_max_attempts'] ?? 5),
            'expires_at' => now()->addMinutes((int) ($security['login_otp_expiry_minutes'] ?? 5)),
            'last_sent_at' => now(),
            'ip_address' => $request->ip(),
            'user_agent' => substr((string) $request->userAgent(), 0, 1000),
            'remember' => $remember,
        ]);

        $sent = $this->dispatchOtp($channel, $user, $otp);
        if (! $sent) {
            $challenge->consumed_at = now();
            $challenge->save();

            throw ValidationException::withMessages([
                'email' => 'Could not send OTP right now. Please try again.',
            ]);
        }

        return $challenge;
    }

    private function dispatchOtp(string $channel, User $user, string $otp): bool
    {
        if ($channel === 'sms') {
            return app(SmsService::class)->sendOtp((string) $user->phone, $otp);
        }

        return app(EmailService::class)->sendOtpEmail((string) $user->email, $otp);
    }

    private function pendingChallenge(Request $request): ?LoginOtpChallenge
    {
        $challengeId = $request->session()->get('auth.pending_otp_challenge_id');
        $userId = $request->session()->get('auth.pending_otp_user_id');

        if (! $challengeId || ! $userId) {
            return null;
        }

        $challenge = LoginOtpChallenge::query()
            ->with('user')
            ->whereKey($challengeId)
            ->where('user_id', $userId)
            ->first();

        return $challenge ?: null;
    }

    private function clearPendingAuth(Request $request): void
    {
        $request->session()->forget('auth.pending_otp_challenge_id');
        $request->session()->forget('auth.pending_otp_user_id');
        $request->session()->forget('auth.login_entry_route');
    }

    private function loginEntryRoute(Request $request): string
    {
        $route = (string) $request->session()->get('auth.login_entry_route', 'login');

        return in_array($route, ['login', 'employee.login'], true) ? $route : 'login';
    }

    private function generateOtp(): string
    {
        return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    }

    private function otpHash(string $otp): string
    {
        return hash_hmac('sha256', $otp, (string) config('app.key'));
    }

    private function securitySettings(): array
    {
        // Bootstrap-safe fallback for fresh/live deployments:
        // If general_settings table/row is missing, do not block admin login behind OTP yet.
        // This allows first login to configure SMTP/SMS + security settings from admin panel.
        try {
            if (! Schema::hasTable('general_settings')) {
                return [
                    'admin_login_otp_enabled' => false,
                    'employee_login_otp_enabled' => false,
                ];
            }

            $hasRow = DB::table('general_settings')->exists();
            if (! $hasRow) {
                return [
                    'admin_login_otp_enabled' => false,
                    'employee_login_otp_enabled' => false,
                ];
            }

            $settings = GeneralSetting::getContent();
            return $settings['security'] ?? [];
        } catch (\Throwable) {
            return [
                'admin_login_otp_enabled' => false,
                'employee_login_otp_enabled' => false,
            ];
        }
    }

    private function maskTarget(string $target, string $channel): string
    {
        if ($target === '') {
            return '';
        }

        if ($channel === 'sms') {
            $len = strlen($target);
            if ($len <= 4) {
                return str_repeat('*', $len);
            }

            return str_repeat('*', max(0, $len - 4)) . substr($target, -4);
        }

        if (! str_contains($target, '@')) {
            return '***';
        }

        [$name, $domain] = explode('@', $target, 2);
        $safeName = strlen($name) <= 2 ? str_repeat('*', strlen($name)) : substr($name, 0, 2) . str_repeat('*', max(0, strlen($name) - 2));

        return "{$safeName}@{$domain}";
    }
}
