from __future__ import annotations

import logging
import typing
import unicodedata

from django.conf import settings
from django.contrib.auth import (
    authenticate,
    get_user_model,
)
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth import login as django_login
from django.contrib.auth import logout as django_logout
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import EmailMultiAlternatives
from django.template import loader
from django.utils.encoding import (
    force_bytes,
    force_str,
)
from django.utils.http import urlsafe_base64_decode as uid_decoder
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import ValidationError

from vq_django_auth.app_settings import api_settings
from vq_django_auth.choices import AuthMethod
from vq_django_auth.dataclasses import PasswordResetContext

UserModel = get_user_model()

logger = logging.getLogger("vq_django_auth.usecases")

if typing.TYPE_CHECKING:
    from rest_framework.request import Request


def _unicode_ci_compare(s1: str, s2: str) -> bool:
    """
    Perform case-insensitive comparison of two identifiers.

    Uses the recommended algorithm from Unicode Technical Report 36, section
    2.11.2(B)(2) for proper internationalization support.

    Args:
        s1: First string to compare
        s2: Second string to compare

    Returns:
        bool: True if strings match case-insensitively, False otherwise
    """
    return (
        unicodedata.normalize("NFKC", s1).casefold()
        == unicodedata.normalize("NFKC", s2).casefold()
    )


def find_user_by_email(email: str) -> AbstractBaseUser | None:
    """
    Find an active user by email address for password reset purposes.

    Given an email, returns the matching active user who has a usable password.
    Returns None if no matching active user is found or if the user's password
    is unusable.

    Args:
        email: Email address to search for

    Returns:
        User: The matching user object or None if no user found
    """
    email_field_name = UserModel.get_email_field_name()
    active_users = UserModel._default_manager.filter(
        **{
            "%s__iexact" % email_field_name: email,
            "is_active": True,
        }
    )
    users = [
        u
        for u in active_users
        if u.has_usable_password()
        and _unicode_ci_compare(email, getattr(u, email_field_name))
    ]
    return None if not users else users[0]


def send_password_reset_email(
    context: PasswordResetContext,
    subject_template_name: str = "registration/password_reset_subject.txt",
    email_template_name_html: str = "password_reset_email.html",
    email_template_name_text: str = "password_reset_email.txt",
) -> None:
    """
    Render and send a password reset email using provided context.

    This is a low-level function that handles the actual email rendering and sending.
    It takes a pre-built context with all necessary variables and sends the email.
    Logs success or failure of the email sending operation.

    Args:
        context: PasswordResetContext dataclass containing email template variables
        subject_template_name: Path to the subject template
        email_template_name: Path to the email body template
    """
    context_dict = context.to_dict()
    raw_subject = loader.render_to_string(subject_template_name, context_dict)
    # Email subject *must not* contain newlines
    subject = "".join(raw_subject.splitlines())
    body = loader.render_to_string(email_template_name_html, context_dict)
    body_text = loader.render_to_string(email_template_name_text, context_dict)

    email_message = EmailMultiAlternatives(
        subject=subject,
        body=body,
        from_email=settings.DEFAULT_FROM_EMAIL,
        to=[context.email],
    )

    email_message.attach_alternative(body_text, "text/plain")

    try:
        email_message.send()
        logger.info("Successfully sent password reset email to %s", context.email)
    except Exception:
        logger.exception("Failed to send password reset email to %s", context.user.pk)


def initiate_password_reset(
    request: Request,
    user_email: str,
    token: str,
    user: AbstractBaseUser,
    use_https: bool = True,
) -> bool:
    """
    Initiate the password reset process for a user.

    This is a high-level orchestration function that handles the complete password
    reset workflow: finds the user by email, creates the necessary context with
    user information, site details, and security tokens, then sends the email
    using the configured email sending function.

    For security reasons, this function will silently fail if no active user with
    a usable password is found for the given email address. This prevents email
    enumeration attacks.

    Args:
        request: The HTTP request object
        user_email: Email address of the user requesting password reset
        use_https: Whether to use HTTPS in the reset link (defaults to True)

    Returns:
        bool: True if an email was sent, False if no user found (silent failure)
    """
    current_site = get_current_site(request)
    site_name = current_site.name
    domain = current_site.domain

    context = PasswordResetContext(
        email=user_email,
        domain=domain,
        site_name=site_name,
        uid=urlsafe_base64_encode(force_bytes(user.pk)),
        user=user,
        front_url=api_settings.FRONT_RESET_PASSWORD_PATH,
        token=token,
        protocol="https" if use_https else "http",
    )
    api_settings.PASSWORD_RESET_SEND_MAIL(context=context)
    return True


def confirm_password_reset(
    request: Request, data: dict[str, str]
) -> typing.Tuple[AbstractBaseUser, Token | None]:
    """
    Confirm a password reset request and set the new password.

    Validates the reset token and UID from the request data,
    then sets the new password for the user if validation succeeds.

    Args:
        request: The HTTP request object
        data: Dictionary containing:
            - uid: Base64 encoded user ID
            - token: Password reset verification token
            - new_password1: New password
            - new_password2: New password confirmation

    Raises:
        ValidationError: If the UID or token is invalid
    """
    # Decode the uidb64 (allauth use base36) to uid to get User object
    try:
        uid = force_str(uid_decoder(data["uid"]))
        user = UserModel._default_manager.get(pk=uid)
    except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
        raise ValidationError({"uid": [_("Invalid value")]})

    if not default_token_generator.check_token(user, data["token"]):
        raise ValidationError({"token": [_("Invalid value")]})

    user.set_password(data["new_password1"])
    user.save()
    logout_user(request=request)

    return authenticate_user(request=request, auto_login_user=user)


def authenticate_user(
    request: Request, auto_login_user: AbstractBaseUser | None = None, **kwargs: str
) -> typing.Tuple[AbstractBaseUser, Token | None]:
    """
    Authenticate a user and create session or token as appropriate.

    Based on settings, either creates a session for the user or
    returns a token for API authentication.

    Args:
        request: The HTTP request object
        user: User to auto login after password change
        **kwargs: Authentication parameters (typically username/email and password)

    Returns:
        tuple: (user, token) where token may be None if using session authentication

    Raises:
        ValidationError: If authentication fails
    """
    token = None
    if auto_login_user is None:
        user = authenticate(request=request, **kwargs)
        if user is None:
            raise ValidationError("Invalid credentials")
    else:
        user = auto_login_user

    if api_settings.AUTH_METHOD is AuthMethod.SESSION:
        django_login(request, user)
    elif api_settings.AUTH_METHOD is AuthMethod.TOKEN:
        token, _ = Token.objects.get_or_create(user=user)

    return user, token


def logout_user(request: Request) -> None:
    """
    Log out a user by invalidating their token and/or session.

    Attempts to delete the user's auth token if one exists,
    and ends the user's session if AUTH_METHOD is set to SESSION.

    Args:
        request: The HTTP request object containing the user to log out
    """

    if api_settings.AUTH_METHOD is AuthMethod.SESSION:
        django_logout(request)
    elif api_settings.AUTH_METHOD is AuthMethod.TOKEN:
        try:
            request.user.auth_token.delete()  # type: ignore[union-attr]
        except ObjectDoesNotExist:
            pass


def change_user_password(
    request: Request, data: dict[str, str]
) -> typing.Tuple[AbstractBaseUser, Token | None]:
    """
    Change a user's password and handle post-change actions.

    Sets the new password for the authenticated user and logs them out
    based on the configuration settings.

    Args:
        request: The HTTP request object with the authenticated user
        data: Dictionary containing:
            - new_password1: The new password to set
            - old_password: The current password (if verification enabled)
    """
    user = request.user
    if not isinstance(user, AbstractBaseUser):
        raise ValidationError("Authentication required")

    user.set_password(data["new_password1"])
    user.save()
    logout_user(request)

    return authenticate_user(request=request, auto_login_user=user)
