# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""Auto-shutdown for an exposed first-run Studio whose admin password is unchanged.

On a fresh install the seeded bootstrap admin password stays a valid login
credential until first login changes it. When the web UI is put on the network
(``--secure`` / ``0.0.0.0``) and nobody completes that first-login change within
a deadline, tear Studio down so a fresh, unconfigured instance does not stay
publicly reachable indefinitely. If the password was changed, Studio keeps
running.

Scope: web UI launches only (never ``--api-only``, which authenticates by API
key rather than the admin password, and never Colab). Configurable via
``UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT`` (seconds; default 3600; ``0`` disables).
"""

import os
import sys
import threading

BOOTSTRAP_TIMEOUT_ENV_VAR = "UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT"
DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS = 3600


def bootstrap_timeout_seconds(env = None) -> int:
    """Resolve the deadline in seconds. ``0`` (or invalid/negative) disables it.

    A malformed value falls back to the default rather than disabling, so a typo
    cannot silently remove the protection.
    """
    env = os.environ if env is None else env
    raw = env.get(BOOTSTRAP_TIMEOUT_ENV_VAR)
    if raw is None or raw.strip() == "":
        return DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS
    try:
        value = int(raw)
    except ValueError:
        return DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS
    return value if value > 0 else 0


def _is_exposed_bind(host: str, secure: bool) -> bool:
    """True when this launch puts the web UI on the network (tunnel or non-loopback)."""
    if secure:
        return True
    if host in ("0.0.0.0", "::"):
        return True
    try:
        from utils.host_policy import is_external_host
    except Exception:
        return False
    return bool(is_external_host(host))


def should_arm_bootstrap_timeout(
    *,
    host: str,
    secure: bool,
    api_only: bool,
    frontend_served: bool,
    is_colab: bool,
    requires_change: bool,
    timeout_seconds: int,
) -> bool:
    """Whether to arm the deadline: only for an exposed web UI whose seeded admin
    password is still unchanged. Pure decision (no I/O) for cheap unit testing."""
    if timeout_seconds <= 0:
        return False
    if api_only or not frontend_served or is_colab:
        return False
    if not requires_change:
        return False
    return _is_exposed_bind(host, secure)


def _format_duration(seconds: int) -> str:
    """Human-friendly duration for the shutdown message (seconds under a minute)."""

    def _plural(n: int, unit: str) -> str:
        return f"{n} {unit}{'' if n == 1 else 's'}"

    if seconds < 60:
        return _plural(seconds, "second")
    minutes, rem = divmod(seconds, 60)
    label = _plural(minutes, "minute")
    if rem:
        label += f" {_plural(rem, 'second')}"
    return label


def enforce_bootstrap_password_deadline(
    storage,
    trigger_shutdown,
    *,
    timeout_seconds: int,
    logger = None,
) -> bool:
    """Deadline handler: shut down iff the seeded admin password is still unchanged.

    Returns True if it shut Studio down, False if it left it running (the
    password was changed in time).
    """
    try:
        still_default = storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME)
    except Exception:
        return False
    if not still_default:
        return False  # password changed in time -> leave Studio running

    message = (
        "\nUnsloth Studio was exposed on the network but its default admin "
        f"password was not changed within {_format_duration(timeout_seconds)}. "
        "Shutting down to avoid leaving an unsecured public instance running.\n"
        "Next time, sign in and change the password on first login, or set "
        f"{BOOTSTRAP_TIMEOUT_ENV_VAR}=0 to disable this timeout."
    )
    if logger is not None:
        logger.warning(message)
    print(message, file = sys.stderr, flush = True)
    try:
        trigger_shutdown()
    except Exception as e:  # shutdown is best-effort; never raise from the timer
        if logger is not None:
            logger.warning("Bootstrap-timeout shutdown failed: %s", e)
    return True


def arm_bootstrap_timeout(
    storage,
    trigger_shutdown,
    *,
    timeout_seconds: int,
    logger = None,
) -> "threading.Timer":
    """Start a daemon timer that enforces the deadline. Returns the Timer."""
    timer = threading.Timer(
        timeout_seconds,
        enforce_bootstrap_password_deadline,
        args = (storage, trigger_shutdown),
        kwargs = {"timeout_seconds": timeout_seconds, "logger": logger},
    )
    timer.daemon = True
    timer.start()
    return timer
