"""User repository for data access."""

from uuid import UUID

from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from identity_plan_kit.auth.domain.entities import User
from identity_plan_kit.auth.models.user import UserModel
from identity_plan_kit.auth.models.user_provider import UserProviderModel
from identity_plan_kit.shared.audit import mask_email
from identity_plan_kit.shared.logging import get_logger

logger = get_logger(__name__)


class UserRepository:
    """Repository for user data access."""

    def __init__(self, session: AsyncSession) -> None:
        self._session = session

    async def get_by_id(self, user_id: UUID) -> User | None:
        """
        Get user by ID.

        Args:
            user_id: User UUID

        Returns:
            User entity or None if not found
        """
        stmt = (
            select(UserModel).options(selectinload(UserModel.role)).where(UserModel.id == user_id)
        )
        result = await self._session.execute(stmt)
        model = result.scalar_one_or_none()

        if model is None:
            return None

        return self._to_entity(model)

    async def get_by_email(
        self,
        email: str,
        for_update: bool = False,
    ) -> User | None:
        """
        Get user by email address.

        Args:
            email: User email
            for_update: If True, lock the row for update

        Returns:
            User entity or None if not found
        """
        stmt = (
            select(UserModel).options(selectinload(UserModel.role)).where(UserModel.email == email)
        )

        if for_update:
            stmt = stmt.with_for_update()

        result = await self._session.execute(stmt)
        model = result.scalar_one_or_none()

        if model is None:
            return None

        return self._to_entity(model)

    async def get_by_provider(
        self,
        provider_code: str,
        external_user_id: str,
    ) -> User | None:
        """
        Get user by OAuth provider.

        Args:
            provider_code: Provider code (e.g., "google")
            external_user_id: User ID from the provider

        Returns:
            User entity or None if not found
        """
        stmt = (
            select(UserModel)
            .join(UserProviderModel)
            .options(selectinload(UserModel.role))
            .where(
                UserProviderModel.code == provider_code,
                UserProviderModel.external_user_id == external_user_id,
            )
        )
        result = await self._session.execute(stmt)
        model = result.scalar_one_or_none()

        if model is None:
            return None

        return self._to_entity(model)

    async def create(
        self,
        email: str,
        role_id: UUID,
        is_verified: bool = False,
    ) -> User:
        """
        Create a new user.

        Args:
            email: User email
            role_id: Role UUID to assign
            is_verified: Whether email is verified

        Returns:
            Created user entity
        """
        model = UserModel(
            email=email,
            role_id=role_id,
            is_verified=is_verified,
        )
        self._session.add(model)
        await self._session.flush()

        # Reload with relationships
        await self._session.refresh(model, ["role"])

        return self._to_entity(model)

    async def create_with_provider(
        self,
        email: str,
        role_id: UUID,
        provider_code: str,
        external_user_id: str,
        is_verified: bool = True,
    ) -> User:
        """
        Create a new user with OAuth provider link.

        Handles race conditions by catching unique constraint violations.

        Args:
            email: User email
            role_id: Role UUID to assign
            provider_code: OAuth provider code
            external_user_id: Provider's user ID
            is_verified: Whether email is verified

        Returns:
            Created user entity

        Raises:
            IntegrityError: If user with email already exists (race condition)
        """
        # Create user (IntegrityError raised if email already exists - race condition)
        user_model = UserModel(
            email=email,
            role_id=role_id,
            is_verified=is_verified,
        )
        self._session.add(user_model)
        await self._session.flush()

        # Create provider link
        provider_model = UserProviderModel(
            user_id=user_model.id,
            code=provider_code,
            external_user_id=external_user_id,
        )
        self._session.add(provider_model)
        await self._session.flush()

        # Reload with relationships
        await self._session.refresh(user_model, ["role", "providers"])

        return self._to_entity(user_model)

    async def get_or_create_with_provider(
        self,
        email: str,
        role_id: UUID,
        provider_code: str,
        external_user_id: str,
        is_verified: bool = True,
    ) -> tuple[User, bool]:
        """
        Get existing user or create new one with provider (race-condition safe).

        Uses database-level locking and savepoints to prevent duplicate user creation
        while maintaining transaction integrity.

        Args:
            email: User email
            role_id: Role UUID for new users
            provider_code: OAuth provider code
            external_user_id: Provider's user ID
            is_verified: Whether email is verified

        Returns:
            Tuple of (user, created) where created is True if user was created
        """
        # First, try to get existing user by provider
        user = await self.get_by_provider(provider_code, external_user_id)
        if user:
            return user, False

        # Try to get by email with lock
        user = await self.get_by_email(email, for_update=True)
        if user:
            # User exists, link provider
            await self.add_provider(user.id, provider_code, external_user_id)
            return user, False

        # No existing user, create new one using savepoint for safe rollback
        try:
            # Use nested transaction (savepoint) so we can rollback without
            # corrupting the outer transaction managed by UoW
            async with self._session.begin_nested():
                user = await self.create_with_provider(
                    email=email,
                    role_id=role_id,
                    provider_code=provider_code,
                    external_user_id=external_user_id,
                    is_verified=is_verified,
                )
                return user, True
        except IntegrityError as e:
            # Race condition - another request created the user
            # Savepoint was automatically rolled back, session is still valid
            logger.warning(
                "user_create_race_condition_handled",
                email=mask_email(email),
                provider=provider_code,
            )
            # Fetch the existing user (created by concurrent request)
            user = await self.get_by_email(email)
            if user:
                # Link provider if not already linked
                existing_provider = await self.get_by_provider(provider_code, external_user_id)
                if not existing_provider:
                    await self.add_provider(user.id, provider_code, external_user_id)
                return user, False
            # This should not happen - raise to surface the issue
            raise RuntimeError(f"User creation race condition but user not found: {email}") from e

    async def add_provider(
        self,
        user_id: UUID,
        provider_code: str,
        external_user_id: str,
    ) -> None:
        """
        Add OAuth provider to existing user.

        Args:
            user_id: User UUID
            provider_code: OAuth provider code
            external_user_id: Provider's user ID
        """
        provider_model = UserProviderModel(
            user_id=user_id,
            code=provider_code,
            external_user_id=external_user_id,
        )
        self._session.add(provider_model)
        await self._session.flush()

    async def update(self, user: User) -> User:
        """
        Update user data.

        Uses SELECT FOR UPDATE to prevent lost update race conditions.

        Args:
            user: User entity with updated data

        Returns:
            Updated user entity
        """
        # P1 fix: Use SELECT FOR UPDATE to prevent lost updates
        stmt = select(UserModel).where(UserModel.id == user.id).with_for_update()
        result = await self._session.execute(stmt)
        model = result.scalar_one()

        model.email = user.email
        model.role_id = user.role_id
        model.is_active = user.is_active
        model.is_verified = user.is_verified

        await self._session.flush()
        await self._session.refresh(model, ["role"])

        return self._to_entity(model)

    async def deactivate(
        self,
        user_id: UUID,
        reason: str | None = None,
    ) -> bool:
        """
        Deactivate a user account.

        P1 SECURITY FIX: Used when token theft is detected to secure the account.
        User must contact support to reactivate.

        Args:
            user_id: User UUID to deactivate
            reason: Reason for deactivation (for logging)

        Returns:
            True if user was deactivated, False if already inactive or not found
        """
        # Import here - update is only needed for bulk operations
        from sqlalchemy import update  # noqa: PLC0415

        stmt = (
            update(UserModel)
            .where(
                UserModel.id == user_id,
                UserModel.is_active == True,  # noqa: E712
            )
            .values(is_active=False)
        )
        result = await self._session.execute(stmt)
        await self._session.flush()

        deactivated = result.rowcount > 0

        if deactivated:
            logger.warning(
                "user_deactivated",
                user_id=str(user_id),
                reason=reason,
            )

        return deactivated

    async def reactivate(
        self,
        user_id: UUID,
        reason: str | None = None,
    ) -> bool:
        """
        Reactivate a user account.

        Args:
            user_id: User UUID to reactivate
            reason: Reason for reactivation (for logging)

        Returns:
            True if user was reactivated, False if already active or not found
        """
        # Import here - update is only needed for bulk operations
        from sqlalchemy import update  # noqa: PLC0415

        stmt = (
            update(UserModel)
            .where(
                UserModel.id == user_id,
                UserModel.is_active == False,  # noqa: E712
            )
            .values(is_active=True)
        )
        result = await self._session.execute(stmt)
        await self._session.flush()

        reactivated = result.rowcount > 0

        if reactivated:
            logger.info(
                "user_reactivated",
                user_id=str(user_id),
                reason=reason,
            )

        return reactivated

    def _to_entity(self, model: UserModel) -> User:
        """Convert ORM model to domain entity."""
        return User(
            id=model.id,
            email=model.email,
            role_id=model.role_id,
            is_active=model.is_active,
            is_verified=model.is_verified,
            created_at=model.created_at,
            updated_at=model.updated_at,
            role_code=model.role.code if model.role else None,
        )
