"""Refresh token repository."""

from datetime import UTC, datetime
from uuid import UUID

from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession

from identity_plan_kit.auth.domain.entities import RefreshToken
from identity_plan_kit.auth.models.refresh_token import RefreshTokenModel
from identity_plan_kit.shared.logging import get_logger

logger = get_logger(__name__)


class RefreshTokenRepository:
    """Repository for refresh token data access."""

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

    async def create(
        self,
        user_id: UUID,
        token_hash: str,
        expires_at: datetime,
        user_agent: str | None = None,
        ip_address: str | None = None,
    ) -> RefreshToken:
        """
        Create a new refresh token.

        Args:
            user_id: User UUID
            token_hash: Hashed token for storage
            expires_at: Token expiration time
            user_agent: Client user agent
            ip_address: Client IP address

        Returns:
            Created refresh token entity
        """
        model = RefreshTokenModel(
            user_id=user_id,
            token_hash=token_hash,
            expires_at=expires_at,
            user_agent=user_agent,
            ip_address=ip_address,
        )
        self._session.add(model)
        await self._session.flush()

        logger.debug(
            "refresh_token_created",
            user_id=str(user_id),
            token_id=str(model.id),
        )

        return self._to_entity(model)

    async def get_by_hash(
        self,
        token_hash: str,
        for_update: bool = False,
    ) -> RefreshToken | None:
        """
        Get refresh token by its hash.

        Args:
            token_hash: Hashed token
            for_update: If True, lock the row for update (prevents race conditions)

        Returns:
            Refresh token entity or None
        """
        stmt = select(RefreshTokenModel).where(
            RefreshTokenModel.token_hash == token_hash,
            RefreshTokenModel.revoked_at.is_(None),
        )

        # Apply row-level lock if requested (P1 race condition fix)
        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 revoke(self, token_id: UUID) -> None:
        """
        Revoke a refresh token.

        Args:
            token_id: Token UUID to revoke
        """
        stmt = (
            update(RefreshTokenModel)
            .where(RefreshTokenModel.id == token_id)
            .values(revoked_at=datetime.now(UTC))
        )
        await self._session.execute(stmt)
        await self._session.flush()

        logger.info("refresh_token_revoked", token_id=str(token_id))

    async def revoke_all_for_user(self, user_id: UUID) -> int:
        """
        Revoke all refresh tokens for a user.

        Used when user changes password or logs out everywhere.

        Args:
            user_id: User UUID

        Returns:
            Number of tokens revoked
        """
        stmt = (
            update(RefreshTokenModel)
            .where(
                RefreshTokenModel.user_id == user_id,
                RefreshTokenModel.revoked_at.is_(None),
            )
            .values(revoked_at=datetime.now(UTC))
        )
        result = await self._session.execute(stmt)
        await self._session.flush()

        count = result.rowcount
        logger.info(
            "refresh_tokens_revoked_all",
            user_id=str(user_id),
            count=count,
        )

        return count

    async def cleanup_expired(self, batch_size: int = 1000) -> int:
        """
        Delete expired and revoked tokens in batches.

        Uses batch deletion to prevent long-running transactions and
        table-level locks on large datasets.

        Should be run periodically via background job.

        Args:
            batch_size: Maximum number of tokens to delete per call

        Returns:
            Number of tokens deleted in this batch
        """
        # Import here - delete is only needed for cleanup operations
        from sqlalchemy import delete  # noqa: PLC0415

        # First, get IDs to delete (with limit)
        select_stmt = (
            select(RefreshTokenModel.id)
            .where(
                (RefreshTokenModel.expires_at < datetime.now(UTC))
                | (RefreshTokenModel.revoked_at.is_not(None))
            )
            .limit(batch_size)
        )
        result = await self._session.execute(select_stmt)
        ids_to_delete = [row[0] for row in result.fetchall()]

        if not ids_to_delete:
            return 0

        # Delete by IDs
        delete_stmt = delete(RefreshTokenModel).where(RefreshTokenModel.id.in_(ids_to_delete))
        await self._session.execute(delete_stmt)
        await self._session.flush()

        count = len(ids_to_delete)
        logger.info(
            "refresh_tokens_cleaned",
            count=count,
            batch_size=batch_size,
            has_more=count == batch_size,
        )

        return count

    def _to_entity(self, model: RefreshTokenModel) -> RefreshToken:
        """Convert ORM model to domain entity."""
        return RefreshToken(
            id=model.id,
            user_id=model.user_id,
            token_hash=model.token_hash,
            expires_at=model.expires_at,
            created_at=model.created_at,
            revoked_at=model.revoked_at,
            user_agent=model.user_agent,
            ip_address=model.ip_address,
        )
