from __future__ import annotations

from app.auth import hash_password, verify_password
from app.core.logging import get_audit_logger, get_logger
from app.database import db


logger = get_logger(__name__)
audit_logger = get_audit_logger()
MIN_PASSWORD_LENGTH = 6


class PasswordServiceError(Exception):
    def __init__(self, user_message: str) -> None:
        super().__init__(user_message)
        self.user_message = user_message


class PasswordPolicyError(PasswordServiceError):
    pass


class UserNotFoundError(PasswordPolicyError):
    def __init__(self) -> None:
        super().__init__("Usuario no encontrado.")


class InvalidCurrentPasswordError(PasswordPolicyError):
    def __init__(self) -> None:
        super().__init__("La contraseña actual no es válida.")


class PasswordConfirmationMismatchError(PasswordPolicyError):
    def __init__(self) -> None:
        super().__init__("La nueva contraseña y su confirmación no coinciden.")


class PasswordTooShortError(PasswordPolicyError):
    def __init__(self) -> None:
        super().__init__(f"La nueva contraseña debe tener al menos {MIN_PASSWORD_LENGTH} caracteres.")


class PasswordUnchangedError(PasswordPolicyError):
    def __init__(self) -> None:
        super().__init__("La nueva contraseña debe ser diferente de la actual.")


class PasswordHashingError(PasswordServiceError):
    def __init__(self) -> None:
        super().__init__("No se pudo asegurar la nueva contraseña.")


class PasswordUpdateError(PasswordServiceError):
    def __init__(self) -> None:
        super().__init__("No se pudo actualizar la contraseña.")


def validate_new_password(new_password: str, confirm_password: str | None = None) -> str:
    if not new_password or len(new_password) < MIN_PASSWORD_LENGTH:
        raise PasswordTooShortError()
    if confirm_password is not None and new_password != confirm_password:
        raise PasswordConfirmationMismatchError()
    return new_password


async def change_own_password(username: str, current_password: str, new_password: str) -> None:
    await _change_password(
        actor_username=username,
        target_username=username,
        new_password=new_password,
        current_password=current_password,
        event_type="auth.password_changed",
        action="change_own_password",
    )


async def reset_user_password(actor_username: str, target_username: str, new_password: str) -> None:
    await _change_password(
        actor_username=actor_username,
        target_username=target_username,
        new_password=new_password,
        current_password=None,
        event_type="auth.password_reset",
        action="reset_user_password",
    )


async def _change_password(
    *,
    actor_username: str,
    target_username: str,
    new_password: str,
    current_password: str | None,
    event_type: str,
    action: str,
) -> None:
    try:
        user_data = await _get_user_or_raise(target_username)
        stored_password = str(user_data.get("password") or "")
        validate_new_password(new_password)

        if current_password is not None and not verify_password(current_password, stored_password):
            raise InvalidCurrentPasswordError()
        if stored_password and verify_password(new_password, stored_password):
            raise PasswordUnchangedError()

        await _persist_password(target_username, new_password)
    except PasswordPolicyError as exc:
        _audit_password_event(
            event_type=event_type,
            action=action,
            outcome="warning",
            actor_username=actor_username,
            target_username=target_username,
            exc=exc,
        )
        raise
    except PasswordServiceError as exc:
        _audit_password_event(
            event_type=event_type,
            action=action,
            outcome="error",
            actor_username=actor_username,
            target_username=target_username,
            exc=exc,
        )
        raise

    _audit_password_event(
        event_type=event_type,
        action=action,
        outcome="success",
        actor_username=actor_username,
        target_username=target_username,
    )


async def _get_user_or_raise(username: str) -> dict:
    user_data = await db.users.find_one({"username": username})
    if not user_data:
        raise UserNotFoundError()
    return user_data


async def _persist_password(username: str, new_password: str) -> None:
    try:
        hashed_password = hash_password(new_password)
    except Exception as exc:  # pragma: no cover - defensive branch
        logger.exception("Password hashing failed for username=%s", username)
        raise PasswordHashingError() from exc

    try:
        result = await db.users.update_one(
            {"username": username},
            {"$set": {"password": hashed_password}},
        )
    except Exception as exc:  # pragma: no cover - defensive branch
        logger.exception("Password update failed for username=%s", username)
        raise PasswordUpdateError() from exc

    if getattr(result, "acknowledged", True) is False or getattr(result, "matched_count", 1) == 0:
        logger.error("Password update was not acknowledged for username=%s", username)
        raise PasswordUpdateError()


def _audit_password_event(
    *,
    event_type: str,
    action: str,
    outcome: str,
    actor_username: str,
    target_username: str,
    exc: Exception | None = None,
) -> None:
    error = {}
    if exc is not None:
        error = {
            "class": exc.__class__.__name__,
            "message": str(exc),
        }

    audit_logger.business_event(
        event_type=event_type,
        action=action,
        outcome=outcome,
        service="password_service",
        resource={
            "actor_username": actor_username,
            "target_username": target_username,
        },
        error=error,
    )
