<?php

namespace App\Http\Middleware;

use App\Support\AuthCookie;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Laravel\Sanctum\PersonalAccessToken;
use Symfony\Component\HttpFoundation\Response;
use Modules\Auth\Entities\User;

class SingleSessionMiddleware
{
    /**
     * Cache TTL for token processing lock (seconds)
     */
    private const PROCESSING_LOCK_TTL = 5;

    /**
     * Cache TTL for token name cache (seconds)
     */
    private const TOKEN_NAME_CACHE_TTL = 60;

    /**
     * Fresh token age threshold (seconds)
     */
    private const FRESH_TOKEN_AGE = 30;

    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        
        if ($user) {
            try {
                $this->enforceSingleSession($user, $request);
            } catch (\Throwable $e) {
                // Log error but don't block the request
                Log::error('SingleSessionMiddleware error', [
                    'user_id' => $user->id,
                    'error' => $e->getMessage(),
                    'trace' => $e->getTraceAsString()
                ]);
            }
        }

        return $next($request);
    }

    /**
     * Enforce single session per user
     */
    private function enforceSingleSession(User $user, Request $request): void
    {
        if (app()->environment('local')) {
            return;
        }

        // Skip single session enforcement during impersonation
        if ($this->isImpersonationRequest($request)) {
            return;
        }

        // Skip single session enforcement for post-impersonation requests
        if ($this->isPostImpersonationRequest($request)) {
            return;
        }

        // Get current token from request (Bearer header or HttpOnly cookie)
        $currentToken = $this->resolveToken($request);
        if (!$currentToken) {
            return;
        }

        // Parse the token to get the token ID and hash
        $tokenParts = explode('|', $currentToken);
        if (count($tokenParts) !== 2) {
            return;
        }

        $tokenId = $tokenParts[0];
        $tokenHash = $tokenParts[1];

        // Find current token record with caching
        $currentTokenRecord = $this->getTokenRecord($tokenId, $tokenHash);
        
        if (!$currentTokenRecord) {
            return;
        }

        // Check if this is an impersonation token - skip enforcement
        if ($currentTokenRecord->name === 'impersonation-token') {
            return;
        }

        // Skip enforcement for fresh login tokens (SSO login or fresh auth-token)
        if ($this->isFreshLoginToken($currentTokenRecord, $user)) {
            return;
        }

        // Use atomic cache lock per user to prevent race conditions
        $lockKey = "single_session_user_{$user->id}";
        $lock = Cache::lock($lockKey, self::PROCESSING_LOCK_TTL);

        if (!$lock->get()) {
            return;
        }

        try {
            // Get all active tokens for this user (excluding impersonation), ordered by newest first
            $activeTokens = PersonalAccessToken::where('tokenable_type', get_class($user))
                ->where('tokenable_id', $user->id)
                ->where('name', '!=', 'impersonation-token')
                ->where(function ($query) {
                    $query->whereNull('expires_at')
                          ->orWhere('expires_at', '>', now());
                })
                ->orderByDesc('created_at')
                ->orderByDesc('id')
                ->get();

            // Keep only the most recently created token (latest login); revoke all others
            if ($activeTokens->count() > 1) {
                $latestToken = $activeTokens->first();
                $idsToDelete = $activeTokens->where('id', '!=', $latestToken->id)->pluck('id');

                PersonalAccessToken::whereIn('id', $idsToDelete)->delete();

                $this->notifyConcurrentSession($user, $request);
            }
        } finally {
            $lock->release();
        }
    }

    /**
     * Get token record with caching
     */
    private function getTokenRecord(string $tokenId, string $tokenHash): ?PersonalAccessToken
    {
        $cacheKey = "token_record_{$tokenId}";
        
        return Cache::remember($cacheKey, self::TOKEN_NAME_CACHE_TTL, function () use ($tokenId, $tokenHash) {
            return PersonalAccessToken::where('id', $tokenId)
                ->where('token', hash('sha256', $tokenHash))
                ->first();
        });
    }

    /**
     * Check if this is an impersonation-related request
     */
    private function isImpersonationRequest(Request $request): bool
    {
        $path = $request->path();
        
        // Check if the request is to impersonation endpoints
        if (str_contains($path, 'impersonate')) {
            return true;
        }
        
        // Check if there's an impersonation header
        if ($request->hasHeader('X-Original-User-Id')) {
            return true;
        }
        
        // Check if the request has impersonation token
        $currentToken = $this->resolveToken($request);
        if ($currentToken) {
            $tokenParts = explode('|', $currentToken);
            if (count($tokenParts) === 2) {
                $tokenId = $tokenParts[0];
                $tokenHash = $tokenParts[1];
                
                // Use cached token record to avoid duplicate queries
                $tokenRecord = $this->getTokenRecord($tokenId, $tokenHash);
                
                if ($tokenRecord && $tokenRecord->name === 'impersonation-token') {
                    return true;
                }
            }
        }
        
        return false;
    }

    /**
     * Check if this is a post-impersonation request (after leaving impersonation)
     */
    private function isPostImpersonationRequest(Request $request): bool
    {
        $path = $request->path();
        
        // Check if this is a request after leaving impersonation
        // Look for requests that have X-Original-User-Id header but are not impersonation endpoints
        if ($request->hasHeader('X-Original-User-Id') && !str_contains($path, 'impersonate')) {
            return true;
        }
        
        return false;
    }

    /**
     * Check if the current token is a fresh login token (SSO login or post-impersonation)
     * This prevents false positives when a user legitimately logs in
     */
    private function isFreshLoginToken(PersonalAccessToken $currentTokenRecord, User $user): bool
    {
        // Check if token was created recently (within threshold)
        $tokenAge = $currentTokenRecord->created_at->diffInSeconds(now());
        
        if ($tokenAge > self::FRESH_TOKEN_AGE) {
            return false; // Token is not fresh
        }

        // For SSO tokens, if they're fresh, skip enforcement (legitimate login)
        if ($currentTokenRecord->name === 'sso-token') {
            return true;
        }

        // Check for both 'authToken' (camelCase - regular login) and 'auth-token' (kebab-case - post-impersonation)
        $isAuthToken = in_array($currentTokenRecord->name, ['authToken', 'auth-token'], true);
        
        if ($isAuthToken) {
            // Check if there are any other auth tokens for this user that were created before this one
            // When logging out properly, all tokens should be deleted, so if there are no older
            // auth tokens, this is likely a fresh login or post-impersonation
            $olderAuthTokens = PersonalAccessToken::where('tokenable_type', get_class($user))
                ->where('tokenable_id', $user->id)
                ->where('id', '!=', $currentTokenRecord->id)
                ->whereIn('name', ['authToken', 'auth-token']) // Check for both naming conventions
                ->where('created_at', '<', $currentTokenRecord->created_at)
                ->where(function ($query) {
                    $query->whereNull('expires_at')
                          ->orWhere('expires_at', '>', now());
                })
                ->exists(); // Use exists() instead of count() for better performance

            // If there are no older auth tokens, this is a fresh login or post-impersonation
            if (!$olderAuthTokens) {
                return true;
            }
        }

        return false;
    }

    /**
     * Notify user about concurrent session
     */
    private function notifyConcurrentSession(User $user, Request $request): void
    {
        // Notification creation removed per user request
        // Concurrent session detection still works, but no notification is created
    }

    /**
     * Resolve Sanctum plain-text token from Authorization header or auth cookie.
     */
    private function resolveToken(Request $request): ?string
    {
        $token = $request->bearerToken();

        if (is_string($token) && $token !== '') {
            return $token;
        }

        $cookieToken = $request->cookie(AuthCookie::name());

        return is_string($cookieToken) && $cookieToken !== '' ? $cookieToken : null;
    }
}
