# app/services/auth_service.py
import logging
from uuid import UUID

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

from app.core.auth import (
    TokenExpiredError,
    TokenInvalidError,
    create_access_token,
    create_refresh_token,
    get_password_hash_async,
    verify_password_async,
)
from app.core.config import settings
from app.core.exceptions import AuthenticationError
from app.models.user import User
from app.schemas.users import Token, UserCreate, UserResponse

logger = logging.getLogger(__name__)


class AuthService:
    """Service for handling authentication operations"""

    @staticmethod
    async def authenticate_user(
        db: AsyncSession, email: str, password: str
    ) -> User | None:
        """
        Authenticate a user with email and password using async password verification.

        Args:
            db: Database session
            email: User email
            password: User password

        Returns:
            User if authenticated, None otherwise
        """
        result = await db.execute(select(User).where(User.email == email))
        user = result.scalar_one_or_none()

        if not user:
            return None

        # Verify password asynchronously to avoid blocking event loop
        if not await verify_password_async(password, user.password_hash):
            return None

        if not user.is_active:
            raise AuthenticationError("User account is inactive")

        return user

    @staticmethod
    async def create_user(db: AsyncSession, user_data: UserCreate) -> User:
        """
        Create a new user.

        Args:
            db: Database session
            user_data: User data

        Returns:
            Created user

        Raises:
            AuthenticationError: If user already exists or creation fails
        """
        try:
            # Check if user already exists
            result = await db.execute(select(User).where(User.email == user_data.email))
            existing_user = result.scalar_one_or_none()
            if existing_user:
                raise AuthenticationError("User with this email already exists")

            # Create new user with async password hashing
            # Hash password asynchronously to avoid blocking event loop
            hashed_password = await get_password_hash_async(user_data.password)

            # Create user object from model
            user = User(
                email=user_data.email,
                password_hash=hashed_password,
                first_name=user_data.first_name,
                last_name=user_data.last_name,
                phone_number=user_data.phone_number,
                is_active=True,
                is_superuser=False,
            )

            db.add(user)
            await db.commit()
            await db.refresh(user)

            logger.info(f"User created successfully: {user.email}")
            return user

        except AuthenticationError:
            # Re-raise authentication errors without rollback
            raise
        except Exception as e:
            # Rollback on any database errors
            await db.rollback()
            logger.error(f"Error creating user: {e!s}", exc_info=True)
            raise AuthenticationError(f"Failed to create user: {e!s}")

    @staticmethod
    def create_tokens(user: User) -> Token:
        """
        Create access and refresh tokens for a user.

        Args:
            user: User to create tokens for

        Returns:
            Token object with access and refresh tokens and user info
        """
        # Generate claims
        claims = {
            "is_superuser": user.is_superuser,
            "email": user.email,
            "first_name": user.first_name,
        }

        # Create tokens
        access_token = create_access_token(subject=str(user.id), claims=claims)

        refresh_token = create_refresh_token(subject=str(user.id))

        # Convert User model to UserResponse schema
        user_response = UserResponse.model_validate(user)

        return Token(
            access_token=access_token,
            refresh_token=refresh_token,
            user=user_response,
            expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES
            * 60,  # Convert minutes to seconds
        )

    @staticmethod
    async def refresh_tokens(db: AsyncSession, refresh_token: str) -> Token:
        """
        Generate new tokens using a refresh token.

        Args:
            db: Database session
            refresh_token: Valid refresh token

        Returns:
            New access and refresh tokens

        Raises:
            TokenExpiredError: If refresh token has expired
            TokenInvalidError: If refresh token is invalid
        """
        from app.core.auth import decode_token, get_token_data

        try:
            # Verify token is a refresh token
            decode_token(refresh_token, verify_type="refresh")

            # Get user ID from token
            token_data = get_token_data(refresh_token)
            user_id = token_data.user_id

            # Get user from database
            result = await db.execute(select(User).where(User.id == user_id))
            user = result.scalar_one_or_none()
            if not user or not user.is_active:
                raise TokenInvalidError("Invalid user or inactive account")

            # Generate new tokens
            return AuthService.create_tokens(user)

        except (TokenExpiredError, TokenInvalidError) as e:
            logger.warning(f"Token refresh failed: {e!s}")
            raise

    @staticmethod
    async def change_password(
        db: AsyncSession, user_id: UUID, current_password: str, new_password: str
    ) -> bool:
        """
        Change a user's password.

        Args:
            db: Database session
            user_id: User ID
            current_password: Current password
            new_password: New password

        Returns:
            True if password was changed successfully

        Raises:
            AuthenticationError: If current password is incorrect or update fails
        """
        try:
            result = await db.execute(select(User).where(User.id == user_id))
            user = result.scalar_one_or_none()
            if not user:
                raise AuthenticationError("User not found")

            # Verify current password asynchronously
            if not await verify_password_async(current_password, user.password_hash):
                raise AuthenticationError("Current password is incorrect")

            # Hash new password asynchronously to avoid blocking event loop
            user.password_hash = await get_password_hash_async(new_password)
            await db.commit()

            logger.info(f"Password changed successfully for user {user_id}")
            return True

        except AuthenticationError:
            # Re-raise authentication errors without rollback
            raise
        except Exception as e:
            # Rollback on any database errors
            await db.rollback()
            logger.error(
                f"Error changing password for user {user_id}: {e!s}", exc_info=True
            )
            raise AuthenticationError(f"Failed to change password: {e!s}")
