"""Authentication service."""

import uuid
from datetime import datetime, timezone
from typing import Optional
import secrets

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

from src.models.user import User
from src.schemas.user import UserCreate, UserResponse
from src.core.security import (
    hash_password,
    verify_password,
    create_access_token,
    create_refresh_token,
    validate_email_format,
)


class AuthenticationError(Exception):
    """Base authentication error."""

    pass


class EmailAlreadyExistsError(AuthenticationError):
    """Email already registered."""

    pass


class InvalidCredentialsError(AuthenticationError):
    """Invalid email or password."""

    pass


class UserNotFoundError(AuthenticationError):
    """User not found."""

    pass


class InvalidPasswordError(AuthenticationError):
    """Invalid old password."""

    pass


class InvalidTokenError(AuthenticationError):
    """Invalid or expired token."""

    pass


# In-memory token store for password reset (in production, use Redis)
_password_reset_tokens: dict[str, str] = {}  # token -> email


async def register_user(
    session: AsyncSession,
    email: str,
    password: str,
    full_name: Optional[str] = None,
) -> User:
    """Register a new user.

    Args:
        session: Database session
        email: User email
        password: User password (will be hashed)
        full_name: Optional full name

    Returns:
        Created user object

    Raises:
        EmailAlreadyExistsError: If email is already registered
        ValueError: If email format is invalid
    """
    # Validate email format
    if not validate_email_format(email):
        raise ValueError("Invalid email format")

    # Check if email already exists
    result = await session.execute(select(User).where(User.email == email))
    if result.scalar_one_or_none():
        raise EmailAlreadyExistsError(f"Email {email} is already registered")

    # Hash password
    password_hash = hash_password(password)

    # Create user
    user = User(
        email=email,
        password_hash=password_hash,
        full_name=full_name,
        is_active=True,
        is_superuser=False,
    )

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

    return user


async def authenticate_user(
    session: AsyncSession,
    email: str,
    password: str,
) -> Optional[User]:
    """Authenticate a user with email and password.

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

    Returns:
        User object if authenticated, None otherwise
    """
    # Find user by email
    result = await session.execute(select(User).where(User.email == email))
    user = result.scalar_one_or_none()

    if not user:
        return None

    if not user.is_active:
        return None

    # Verify password
    if not verify_password(password, user.password_hash):
        return None

    # Update last login
    user.last_login = datetime.now(timezone.utc)
    await session.commit()

    return user


async def change_password(
    session: AsyncSession,
    user_id: uuid.UUID,
    old_password: str,
    new_password: str,
) -> bool:
    """Change user password.

    Args:
        session: Database session
        user_id: User ID
        old_password: Current password
        new_password: New password

    Returns:
        True if password was changed successfully

    Raises:
        UserNotFoundError: If user not found
        InvalidPasswordError: If old password is incorrect
    """
    result = await session.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()

    if not user:
        raise UserNotFoundError("User not found")

    # Verify old password
    if not verify_password(old_password, user.password_hash):
        raise InvalidPasswordError("Current password is incorrect")

    # Hash and set new password
    user.password_hash = hash_password(new_password)
    await session.commit()

    return True


async def reset_password_request(
    session: AsyncSession,
    email: str,
) -> str:
    """Request a password reset.

    Args:
        session: Database session
        email: User email

    Returns:
        Reset token (to be sent via email)

    Note:
        Always returns a token even if email doesn't exist (security)
    """
    # Generate secure random token
    token = secrets.token_urlsafe(32)

    # Check if user exists
    result = await session.execute(select(User).where(User.email == email))
    user = result.scalar_one_or_none()

    if user:
        # Store token (in production, use Redis with expiration)
        _password_reset_tokens[token] = email

    return token


async def reset_password(
    session: AsyncSession,
    token: str,
    new_password: str,
) -> bool:
    """Reset password using a token.

    Args:
        session: Database session
        token: Reset token
        new_password: New password

    Returns:
        True if password was reset successfully

    Raises:
        InvalidTokenError: If token is invalid or expired
        UserNotFoundError: If user not found
    """
    # Verify token
    email = _password_reset_tokens.get(token)
    if not email:
        raise InvalidTokenError("Invalid or expired token")

    # Find user
    result = await session.execute(select(User).where(User.email == email))
    user = result.scalar_one_or_none()

    if not user:
        raise UserNotFoundError("User not found")

    # Update password
    user.password_hash = hash_password(new_password)
    await session.commit()

    # Remove used token
    del _password_reset_tokens[token]

    return True


async def get_user_by_id(
    session: AsyncSession,
    user_id: uuid.UUID,
) -> Optional[User]:
    """Get user by ID.

    Args:
        session: Database session
        user_id: User ID

    Returns:
        User object or None
    """
    result = await session.execute(select(User).where(User.id == user_id))
    return result.scalar_one_or_none()


async def get_user_by_email(
    session: AsyncSession,
    email: str,
) -> Optional[User]:
    """Get user by email.

    Args:
        session: Database session
        email: User email

    Returns:
        User object or None
    """
    result = await session.execute(select(User).where(User.email == email))
    return result.scalar_one_or_none()


def create_tokens_for_user(user: User) -> tuple[str, str]:
    """Create access and refresh tokens for a user.

    Args:
        user: User object

    Returns:
        Tuple of (access_token, refresh_token)
    """
    token_data = {
        "sub": str(user.id),
        "email": user.email,
    }

    access_token = create_access_token(token_data)
    refresh_token = create_refresh_token(token_data)

    return access_token, refresh_token
