import logging
import secrets
import string
from contextlib import nullcontext
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
from http import HTTPStatus
from typing import Any, Callable, Type

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
from django.db import IntegrityError, transaction
from django.utils import timezone
from django.utils.module_loading import import_string

from django_workflows.services.email import send_email

logger = logging.getLogger(__name__)

DEFAULT_USER_META_MODEL = "users.models_user_meta.UserMeta"
_AMBIGUOUS_CODE_CHARS = set("IO01")
_ALLOWED_CODE_CHARS = [
    c for c in (string.ascii_uppercase + string.digits) if c not in _AMBIGUOUS_CODE_CHARS
]
_LOG_DEV_CODES_SETTING = "LOG_VERIFICATION_CODES_IN_DEBUG"
_EMAIL_DELIVERY_ENABLED_SETTING = "ENABLE_EMAIL_DELIVERY"


def generate_verification_code(*, length: int = 6) -> str:
    return "".join(secrets.choice(_ALLOWED_CODE_CHARS) for _ in range(length))


def _redact_code(code: str) -> str:
    if not code:
        return ""
    if len(code) <= 2:
        return "*" * len(code)
    if len(code) <= 4:
        return f"{code[0]}{'*' * (len(code) - 2)}{code[-1]}"
    return f"{code[:2]}{'*' * (len(code) - 4)}{code[-2:]}"


def _log_code_for_debug(*, label: str, code: str) -> None:
    # Explicit opt-in for local development only.
    should_log = _is_enabled(_get_workflow_setting(_LOG_DEV_CODES_SETTING, False))
    if should_log and logger.isEnabledFor(logging.DEBUG):
        logger.debug("%s code (masked): %s", label, _redact_code(code))


def _is_delivery_success(response: Any) -> bool:
    status_code = getattr(response, "status_code", None)
    return isinstance(status_code, int) and 200 <= status_code < 300


def _registration_atomic_context():
    if settings.configured:
        return transaction.atomic()
    return nullcontext()


def _register_on_commit(callback: Callable[[], None]) -> bool:
    if not settings.configured:
        return False

    try:
        connection = transaction.get_connection()
    except Exception:
        return False

    if not connection.in_atomic_block:
        return False

    transaction.on_commit(callback)
    return True


def _send_verification_email_or_compensate(*, user: Any, email: str, html: str, enabled: bool) -> bool:
    response = send_email(
        to=[user.email],
        subject="Verify your account",
        html=html,
        enabled=enabled,
    )

    if _is_delivery_success(response):
        logger.info("Verification email sent to %s", user.email)
        return True

    logger.warning(
        "Verification email could not be delivered to %s (response_code=%s)",
        user.email,
        getattr(response, "status_code", None),
    )

    # Compensating action: remove the unverified account so registration can be retried.
    try:
        user.delete()
    except Exception:
        logger.exception(
            "Failed to delete user %s after verification delivery failure",
            email,
        )

    return False


def _workflows_settings() -> dict[str, Any]:
    if not settings.configured:
        return {}

    config = getattr(settings, "WORKFLOWS", {}) or {}
    if not isinstance(config, dict):
        logger.warning("settings.WORKFLOWS must be a dict, got %s", type(config).__name__)
        return {}

    return config


def _get_workflow_setting(name: str, default: Any = None) -> Any:
    return _workflows_settings().get(name, default)


def _is_enabled(value: Any) -> bool:
    if isinstance(value, str):
        return value.strip().lower() == "true"
    return bool(value)


def _as_int(value: Any, default: int) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _get_user_meta_model() -> Type[Any]:
    model_path = _get_workflow_setting("USER_META_MODEL", DEFAULT_USER_META_MODEL)
    if not isinstance(model_path, str) or "." not in model_path:
        raise ImproperlyConfigured(
            "WORKFLOWS['USER_META_MODEL'] must be a dotted path like 'myapp.models.UserMeta'."
        )

    try:
        return import_string(model_path)
    except Exception as exc:
        raise ImproperlyConfigured(
            f"Unable to import WORKFLOWS['USER_META_MODEL']: {model_path!r}"
        ) from exc

class VerifyCodeResult(Enum):
    SUCCESS = "success"
    INVALID = "invalid"
    EXPIRED = "expired"
    TOO_MANY_ATTEMPTS = "too_many_attempts"

@dataclass(frozen=True)
class ServiceResult:
    success: bool
    message: str | None = None

@dataclass
class VerifyCodeOutcome:
    result: VerifyCodeResult
    attempts_remaining: int | None = None


@dataclass(frozen=True)
class VerifyCodeHttpResult:
    status_code: int
    payload: dict

@dataclass(frozen=True)
class ChangePasswordResult:
    success: bool
    message: str


def register_user_and_send_verification_email(email: str, first_name: str, last_name: str, password: str) -> ServiceResult:
    """
    Registers a new user and sends a verification email.
    """

    User = get_user_model()

    company_name = _get_workflow_setting("COMPANY_NAME", "")
    ttl_minutes = _as_int(_get_workflow_setting("RESET_CODE_TTL_MINUTES", 10), 10)
    enable_email_delivery = _is_enabled(
        _get_workflow_setting(_EMAIL_DELIVERY_ENABLED_SETTING, False)
    )

    if User.objects.filter(email=email).exists():
        return ServiceResult(
            success=False,
            message="A user with this email already exists.",
        )

    user = None
    verification_code = ""
    message = ""

    try:
        with _registration_atomic_context():
            user = User.objects.create_user(
                email=email,
                username=email,
                first_name=first_name,
                last_name=last_name,
                password=password,
                is_active=False,  # User must verify email before activating
            )

            verification_code = generate_verification_code(length=6)
            UserMeta = _get_user_meta_model()

            meta, _ = UserMeta.objects.get_or_create(user=user)
            meta.verify_code = verification_code
            meta.verify_time = timezone.now()
            meta.save()

            message = (
                f"Hello {first_name} {last_name},<br><br>"
                f"Your account verification code is: <strong>{verification_code}</strong><br /><br />"
                f"The code is valid for {ttl_minutes} minutes.<br><br />"
                f"Thank you!<br><br />"
                f"{company_name}"
            )
    except IntegrityError:
        logger.exception("Registration failed due to IntegrityError for email=%s", email)
        return ServiceResult(
            success=False,
            message="Email already exists, login or reset password.",
        )
    except Exception:
        logger.exception("Unexpected error during registration for email=%s", email)
        return ServiceResult(
            success=False,
            message="An unexpected error occurred during registration.",
        )

    if enable_email_delivery:
        def _send_after_commit() -> None:
            _send_verification_email_or_compensate(
                user=user,
                email=email,
                html=message,
                enabled=enable_email_delivery,
            )

        if _register_on_commit(_send_after_commit):
            logger.info(
                "Verification email send scheduled after transaction commit for %s",
                user.email,
            )
        else:
            if not _send_verification_email_or_compensate(
                user=user,
                email=email,
                html=message,
                enabled=enable_email_delivery,
            ):
                return ServiceResult(
                    success=False,
                    message="We could not send a verification email. Please try again later.",
                )
    else:
        logger.info("Email delivery is disabled. Verification email not sent.")
        _log_code_for_debug(label="Verification", code=verification_code)

    return ServiceResult(
        success=True,
        message="Registration successful. Please check your email for the verification code.",
    )

def send_forgot_password_code(email: str) -> ServiceResult:
    """
    Generates and sends a password reset code.
    Always succeeds silently (even if user does not exist).
    """

    User = get_user_model()

    company_name = _get_workflow_setting("COMPANY_NAME", "")
    ttl_minutes = _as_int(_get_workflow_setting("RESET_CODE_TTL_MINUTES", 10), 10)
    enable_email_delivery = _is_enabled(
        _get_workflow_setting(_EMAIL_DELIVERY_ENABLED_SETTING, False)
    )

    try:
        user = User.objects.get(email=email)
    except User.DoesNotExist:
        logger.info(
            "Forgot password requested for non-existent email: %s",
            email,
        )
        return ServiceResult(
            success=True,
            message="If the email exists, a reset code has been sent.",
        )

    UserMeta = _get_user_meta_model()
    meta, _ = UserMeta.objects.get_or_create(user=user)

    code = generate_verification_code(length=6)

    meta.verify_code = code
    meta.verify_time = timezone.now()
    meta.attempts = 0
    meta.save()

    message = (
        f"Hello {user.first_name} {user.last_name},<br><br>"
        f"Your password reset code is: <strong>{code}</strong><br /><br />"
        f"The code is valid for {ttl_minutes} minutes.<br><br>"
        f"Thank you!<br><br>"
        f"{company_name}"
    )

    if enable_email_delivery:
        response = send_email(
            to=[user.email],
            subject="Reset your password",
            html=message,
            enabled=enable_email_delivery,
        )
        if _is_delivery_success(response):
            logger.info("Password reset code sent to %s", user.email)
        else:
            logger.warning(
                "Password reset email could not be delivered to %s (response_code=%s)",
                user.email,
                getattr(response, "status_code", None),
            )
    else:
        logger.info("Email delivery is disabled. Password reset email not sent.")
        _log_code_for_debug(label="Password reset", code=code)

    return ServiceResult(
        success=True,
        message="If the email exists, a reset code has been sent.",
    )

def change_password(*, email: str, password: str, password_verify: str) -> ChangePasswordResult:
    if not email or not password:
        return ChangePasswordResult(False, "email and password are required.")

    if password != password_verify:
        return ChangePasswordResult(False, "Password and password verification do not match.")

    User = get_user_model()
    try:
        user = User.objects.get(email=email)
    except User.DoesNotExist:
        return ChangePasswordResult(False, "Invalid user.")

    user.set_password(password)
    user.save()

    UserMeta = _get_user_meta_model()
    meta, _ = UserMeta.objects.get_or_create(user=user)
    if hasattr(meta, "force_password_reset"):
        meta.force_password_reset = False
    meta.save()

    logger.info(f"Password changed for {email}")
    return ChangePasswordResult(True, "Password changed successfully.")


def verify_reset_code(*, email: str, code: str) -> VerifyCodeHttpResult:
    """API/controller-friendly wrapper around `validate_reset_code`.

    Returns a payload suitable for DRF `Response(...)` and an integer HTTP status code.
    """

    normalized_code = (code or "").upper()
    outcome = validate_reset_code(email=email, code=normalized_code)

    if outcome.result == VerifyCodeResult.SUCCESS:
        return VerifyCodeHttpResult(
            status_code=int(HTTPStatus.OK),
            payload={"detail": "Code verified."},
        )

    if outcome.result == VerifyCodeResult.INVALID:
        payload: dict = {"detail": "Invalid code. Please check your code and try again."}
        if outcome.attempts_remaining is not None:
            payload["attempts_remaining"] = outcome.attempts_remaining

        return VerifyCodeHttpResult(
            status_code=int(HTTPStatus.ACCEPTED),
            payload=payload,
        )

    if outcome.result == VerifyCodeResult.EXPIRED:
        return VerifyCodeHttpResult(
            status_code=int(HTTPStatus.BAD_REQUEST),
            payload={"detail": "Code expired. Please request a new code."},
        )

    if outcome.result == VerifyCodeResult.TOO_MANY_ATTEMPTS:
        return VerifyCodeHttpResult(
            status_code=int(HTTPStatus.BAD_REQUEST),
            payload={"detail": "Too many attempts. Please request a new code."},
        )

    logger.warning("Unhandled VerifyCodeResult: %s", outcome.result)
    return VerifyCodeHttpResult(
        status_code=int(HTTPStatus.BAD_REQUEST),
        payload={"detail": "Unable to verify code."},
    )

def validate_reset_code(email: str, code: str) -> VerifyCodeOutcome:
    User = get_user_model()
    ttl_minutes = _as_int(_get_workflow_setting("RESET_CODE_TTL_MINUTES", 10), 10)
    max_attempts = _as_int(_get_workflow_setting("MAX_VERIFY_ATTEMPTS", 3), 3)

    try:
        user = User.objects.get(email=email)
    except User.DoesNotExist:
        return VerifyCodeOutcome(result=VerifyCodeResult.INVALID)

    UserMeta = _get_user_meta_model()
    meta, _ = UserMeta.objects.get_or_create(user=user)

    if not meta.verify_time or timezone.now() > meta.verify_time + timedelta(minutes=ttl_minutes):
        _clear_meta(meta)
        return VerifyCodeOutcome(result=VerifyCodeResult.EXPIRED)

    if not meta.verify_code:
        _clear_meta(meta)
        return VerifyCodeOutcome(result=VerifyCodeResult.INVALID)

    attempts = int(getattr(meta, "attempts", 0) or 0)

    if attempts >= max_attempts:
        _clear_meta(meta)
        return VerifyCodeOutcome(result=VerifyCodeResult.TOO_MANY_ATTEMPTS)

    if meta.verify_code != code:
        attempts += 1
        meta.attempts = attempts
        meta.save()

        if attempts >= max_attempts:
            _clear_meta(meta)
            return VerifyCodeOutcome(result=VerifyCodeResult.TOO_MANY_ATTEMPTS)

        return VerifyCodeOutcome(
            result=VerifyCodeResult.INVALID,
            attempts_remaining=max_attempts - attempts,
        )

    # success
    _clear_meta(meta)

    # Activate user upon successful verification
    user.is_active = True
    user.save()

    return VerifyCodeOutcome(result=VerifyCodeResult.SUCCESS)


def _clear_meta(meta: Any) -> None:
    meta.verify_code = None
    meta.verify_time = None
    meta.attempts = 0
    meta.save()
