<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Api\Controller;

use App\Administration\Domain\Event\SessionInvalidee;
use App\Administration\Domain\Event\ToutesSessionsInvalidees;
use App\Administration\Domain\Exception\SessionNotFoundException;
use App\Administration\Domain\Model\RefreshToken\RefreshToken;
use App\Administration\Domain\Model\RefreshToken\TokenFamilyId;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\RefreshTokenRepository;
use App\Administration\Domain\Repository\SessionRepository;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;

use function array_map;
use function count;

use DateTimeInterface;
use InvalidArgumentException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;

/**
 * Sessions management endpoints.
 *
 * Architecture note: Events (SessionInvalidee, ToutesSessionsInvalidees) are dispatched
 * directly from this controller rather than from domain aggregates. This is a documented
 * exception to the standard DDD pattern because:
 * - Session is a read model without command/aggregate behavior
 * - The "invalidate" operation spans multiple aggregates (RefreshToken + Session)
 * - The controller orchestrates infrastructure operations, making it the natural
 *   event emission point for audit trail purposes
 *
 * @see Story 1.6 - Gestion des sessions
 */
final readonly class SessionsController
{
    public function __construct(
        private Security $security,
        private SessionRepository $sessionRepository,
        private RefreshTokenRepository $refreshTokenRepository,
        private MessageBusInterface $eventBus,
        private Clock $clock,
    ) {
    }

    /**
     * List all active sessions for the current user.
     */
    #[Route('/api/me/sessions', name: 'api_sessions_list', methods: ['GET'])]
    public function list(Request $request): JsonResponse
    {
        $user = $this->getSecurityUser();
        $userId = UserId::fromString($user->userId());
        $tenantId = TenantId::fromString($user->tenantId());

        // Get the current family ID from the refresh token cookie
        $currentFamilyId = $this->getCurrentFamilyId($request, $userId);

        $sessions = $this->sessionRepository->findAllByUserId($userId);

        $sessionsData = [];
        foreach ($sessions as $session) {
            // Skip sessions from other tenants (defense in depth)
            if (!$session->tenantId->equals($tenantId)) {
                continue;
            }

            $isCurrent = $currentFamilyId !== null && $session->isCurrent($currentFamilyId);

            $sessionsData[] = [
                'family_id' => (string) $session->familyId,
                'device' => $session->deviceInfo->device,
                'browser' => $session->deviceInfo->browser,
                'os' => $session->deviceInfo->os,
                'location' => $session->location->format(),
                'created_at' => $session->createdAt->format(DateTimeInterface::ATOM),
                'last_activity_at' => $session->lastActivityAt->format(DateTimeInterface::ATOM),
                'is_current' => $isCurrent,
            ];
        }

        // Sort: current session first, then by last activity (most recent first)
        usort($sessionsData, static function (array $a, array $b): int {
            if ($a['is_current'] && !$b['is_current']) {
                return -1;
            }
            if (!$a['is_current'] && $b['is_current']) {
                return 1;
            }

            return $b['last_activity_at'] <=> $a['last_activity_at'];
        });

        return new JsonResponse(['sessions' => $sessionsData]);
    }

    /**
     * Revoke a specific session.
     */
    #[Route('/api/me/sessions/{familyId}', name: 'api_sessions_revoke', methods: ['DELETE'])]
    public function revoke(string $familyId, Request $request): JsonResponse
    {
        $user = $this->getSecurityUser();
        $userId = UserId::fromString($user->userId());

        try {
            $targetFamilyId = TokenFamilyId::fromString($familyId);
            $session = $this->sessionRepository->getByFamilyId($targetFamilyId);
        } catch (InvalidArgumentException|SessionNotFoundException) {
            throw new NotFoundHttpException('Session not found');
        }

        // Verify the session belongs to the user (return 404 to not leak existence)
        if (!$session->belongsToUser($userId)) {
            throw new NotFoundHttpException('Session not found');
        }

        // Prevent revoking current session via this endpoint (use logout instead)
        $currentFamilyId = $this->getCurrentFamilyId($request, $userId);
        if ($currentFamilyId !== null && $session->isCurrent($currentFamilyId)) {
            throw new AccessDeniedHttpException('Cannot revoke current session. Use logout instead.');
        }

        // Invalidate the token family (disconnects the device)
        $this->refreshTokenRepository->invalidateFamily($targetFamilyId);

        // Delete the session
        $this->sessionRepository->delete($targetFamilyId);

        // Dispatch event for audit trail
        $this->eventBus->dispatch(new SessionInvalidee(
            userId: $user->userId(),
            familyId: (string) $targetFamilyId,
            tenantId: TenantId::fromString($user->tenantId()),
            ipAddress: $request->getClientIp() ?? 'unknown',
            userAgent: $request->headers->get('User-Agent', 'unknown'),
            occurredOn: $this->clock->now(),
        ));

        return new JsonResponse(['message' => 'Session revoked'], Response::HTTP_OK);
    }

    /**
     * Revoke all sessions except the current one.
     */
    #[Route('/api/me/sessions', name: 'api_sessions_revoke_all', methods: ['DELETE'])]
    public function revokeAll(Request $request): JsonResponse
    {
        $user = $this->getSecurityUser();
        $userId = UserId::fromString($user->userId());

        // Get the current family ID to exclude it
        $currentFamilyId = $this->getCurrentFamilyId($request, $userId);

        if ($currentFamilyId === null) {
            throw new AccessDeniedHttpException('Unable to identify current session');
        }

        // Delete all sessions except current
        $deletedFamilyIds = $this->sessionRepository->deleteAllExcept($userId, $currentFamilyId);

        // Invalidate all corresponding token families
        foreach ($deletedFamilyIds as $familyId) {
            $this->refreshTokenRepository->invalidateFamily($familyId);
        }

        // Dispatch event for audit trail (only if sessions were actually revoked)
        if (count($deletedFamilyIds) > 0) {
            $this->eventBus->dispatch(new ToutesSessionsInvalidees(
                userId: $user->userId(),
                invalidatedFamilyIds: array_map(static fn (TokenFamilyId $id): string => (string) $id, $deletedFamilyIds),
                exceptFamilyId: (string) $currentFamilyId,
                tenantId: TenantId::fromString($user->tenantId()),
                ipAddress: $request->getClientIp() ?? 'unknown',
                userAgent: $request->headers->get('User-Agent', 'unknown'),
                occurredOn: $this->clock->now(),
            ));
        }

        return new JsonResponse([
            'message' => 'All other sessions revoked',
            'revoked_count' => count($deletedFamilyIds),
        ], Response::HTTP_OK);
    }

    private function getSecurityUser(): SecurityUser
    {
        $user = $this->security->getUser();

        if (!$user instanceof SecurityUser) {
            throw new AccessDeniedHttpException('Authentication required');
        }

        return $user;
    }

    /**
     * Get the current session's family ID from the refresh token cookie.
     *
     * Security: Validates that the refresh token belongs to the authenticated user
     * to prevent cross-account issues in multi-tab scenarios where JWT and cookie
     * could belong to different users.
     */
    private function getCurrentFamilyId(Request $request, UserId $authenticatedUserId): ?TokenFamilyId
    {
        $refreshTokenValue = $request->cookies->get('refresh_token');

        if ($refreshTokenValue === null) {
            return null;
        }

        try {
            $tokenId = RefreshToken::extractIdFromTokenString($refreshTokenValue);
            $refreshToken = $this->refreshTokenRepository->find($tokenId);

            if ($refreshToken === null) {
                return null;
            }

            // Verify the refresh token belongs to the authenticated user
            // This prevents misidentification in multi-tab/account-switch scenarios
            if (!$refreshToken->userId->equals($authenticatedUserId)) {
                return null;
            }

            return $refreshToken->familyId;
        } catch (InvalidArgumentException) {
            return null;
        }
    }
}
