"""Software environment detection and initialization.

This module handles detection and setup of software-level environment variables
that are essential for sageLLM operation, such as:
- HuggingFace Hub connectivity (HF_ENDPOINT)
- Model caching directories
- Network proxies
- Logging levels
"""

from __future__ import annotations

import ctypes
import logging
import os
import shutil
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from urllib import error as urllib_error
from urllib import request as urllib_request

logger = logging.getLogger(__name__)


def _prepend_existing_path(existing: str, candidates: list[Path]) -> str:
    """Prepend existing path segments with validated candidate directories."""
    seen: set[str] = set()
    merged: list[str] = []

    for path in candidates:
        path_str = str(path)
        if not path.is_dir() or path_str in seen:
            continue
        merged.append(path_str)
        seen.add(path_str)

    for segment in existing.split(os.pathsep):
        if not segment or segment in seen:
            continue
        merged.append(segment)
        seen.add(segment)

    return os.pathsep.join(merged)


def _iter_ascend_arch_lib_dirs(toolkit_home: Path) -> list[Path]:
    """Return existing arch-specific Ascend library directories."""
    dirs: list[Path] = []
    for arch in ("aarch64-linux", "x86_64-linux", "arm64-linux"):
        lib_dir = toolkit_home / arch / "lib64"
        plugin_dir = lib_dir / "plugin"
        if lib_dir.is_dir():
            dirs.append(lib_dir)
        if plugin_dir.is_dir():
            dirs.append(plugin_dir)
    return dirs


def _preload_ascend_shared_libraries(toolkit_home: Path) -> None:
    """Preload critical Ascend shared libraries for current process.

    Some systems ignore runtime updates to ``LD_LIBRARY_PATH`` for later
    extension loading. Preloading key libraries by absolute path avoids
    ``libhccl.so`` resolution failures when importing ``torch_npu``.
    """
    search_dirs: list[Path] = [toolkit_home / "hccl" / "lib64"]
    search_dirs.extend(_iter_ascend_arch_lib_dirs(toolkit_home))
    search_dirs.extend(
        [
            toolkit_home / "runtime" / "lib64",
            toolkit_home / "fwkacllib" / "lib64",
        ]
    )

    lib_names = (
        "libmsprofiler.so",
        "libhccl_alg.so",
        "libhccd.so",
        "libhccl.so",
        "libascendcl.so",
    )

    loaded_any = False
    seen: set[str] = set()
    for lib_name in lib_names:
        for lib_dir in search_dirs:
            lib_path = lib_dir / lib_name
            lib_path_str = str(lib_path)
            if not lib_path.is_file() or lib_path_str in seen:
                continue
            seen.add(lib_path_str)
            try:
                ctypes.CDLL(lib_path_str, mode=ctypes.RTLD_GLOBAL)
                loaded_any = True
            except OSError:
                logger.debug("Failed to preload Ascend lib: %s", lib_path, exc_info=True)

    if loaded_any:
        logger.debug("Ascend shared libraries preloaded successfully from %s", toolkit_home)


def _iter_ascend_toolkit_homes() -> list[Path]:
    """Return candidate Ascend toolkit homes ordered by confidence."""
    toolkit_root = Path("/usr/local/Ascend/ascend-toolkit")
    if not toolkit_root.exists():
        return []

    candidates: list[Path] = []
    seen: set[str] = set()

    def _append(path: Path | None) -> None:
        if path is None:
            return
        path_str = str(path)
        if path_str in seen or not path.is_dir():
            return
        seen.add(path_str)
        candidates.append(path)

    _append(toolkit_root / "latest")

    for env_key in ("ASCEND_HOME", "ASCEND_HOME_PATH", "ASCEND_RUNTIME_PATH"):
        env_value = os.environ.get(env_key)
        if not env_value:
            continue
        env_path = Path(env_value)
        if env_key == "ASCEND_RUNTIME_PATH":
            _append(env_path.parent)
        else:
            _append(env_path)

    for child in sorted(toolkit_root.iterdir(), reverse=True):
        if child.name == "latest":
            continue
        _append(child)

    # Accept canonical layout (opp + runtime variant) or arch-specific layout,
    # then rank by completeness. This avoids preferring an incomplete "latest"
    # directory (e.g. missing hccl/lib64) when a versioned toolkit is valid.
    _arch_names = ("aarch64-linux", "x86_64-linux", "arm64-linux")

    def _is_valid(home: Path) -> bool:
        canonical_ok = (home / "opp").is_dir() and any(
            (home / d).is_dir() for d in ("runtime", "fwkacllib", "hccl", "lib64")
        )
        arch_ok = any(
            (home / arch / "lib64").is_dir() or (home / arch / "lib64" / "plugin").is_dir()
            for arch in _arch_names
        )
        return canonical_ok or arch_ok

    def _score(home: Path) -> tuple[int, int, int, int, int, int]:
        has_hccl = (home / "hccl" / "lib64").is_dir() or any(
            (home / arch / "lib64" / "libhccl.so").exists() for arch in _arch_names
        )
        return (
            1 if has_hccl else 0,
            1 if (home / "runtime" / "lib64").is_dir() else 0,
            1 if (home / "fwkacllib" / "lib64").is_dir() else 0,
            1 if (home / "opp").is_dir() else 0,
            1 if home.name != "latest" else 0,
            -candidates.index(home),
        )

    valid_candidates = [home for home in candidates if _is_valid(home)]
    return sorted(valid_candidates, key=_score, reverse=True)


# ---------------------------------------------------------------------------
# Ascend plugin home discovery
# ---------------------------------------------------------------------------


def _discover_ascend_plugin_home() -> Path | None:
    """Discover the Ascend tfplugin home directory.

    Handles ``latest`` symlink as well as versioned sub-directories
    under ``/usr/local/Ascend/tfplugin/`` so that machines without a
    ``latest`` symlink are still covered.
    """
    plugin_root = Path("/usr/local/Ascend/tfplugin")
    if not plugin_root.is_dir():
        return None

    # Prefer explicit 'latest' symlink / directory
    latest = plugin_root / "latest"
    if latest.is_dir():
        return latest

    # Fall back to versioned directories (pick the lexicographically latest)
    try:
        versioned = sorted(
            [p for p in plugin_root.iterdir() if p.is_dir()],
            reverse=True,
        )
    except OSError:
        return None

    return versioned[0] if versioned else None


# ---------------------------------------------------------------------------
# Ascend env-parity check
# ---------------------------------------------------------------------------

#: Minimal set of Ascend environment variables that must align with set_env.sh.
ASCEND_CRITICAL_VARS: tuple[str, ...] = (
    "ASCEND_HOME",
    "ASCEND_TOOLKIT_HOME",
    "ASCEND_RUNTIME_PATH",
    "ASCEND_OPP_PATH",
    "ASCEND_AICPU_PATH",
    "ASCEND_PLUGIN_HOME",
    "PYTHONPATH",
    "LD_LIBRARY_PATH",
)

_PARITY_SENTINEL = object()  # sentinel for "auto-discover toolkit_home"


@dataclass
class AscendVarCheck:
    """Parity-check result for one Ascend environment variable.

    Attributes:
        name: Variable name (e.g. ``ASCEND_HOME``).
        current: Current value from ``os.environ``, or ``None`` if unset.
        expected: Expected value derived from toolkit layout, or ``None``
            when the variable is optional / layout-specific.
        status: One of ``"ok"``, ``"missing"``, ``"abnormal"``,
            ``"optional-absent"``, ``"no-toolkit"``.
        repair: Suggested ``export`` shell command, or ``None``.
    """

    name: str
    current: str | None
    expected: str | None
    status: str
    repair: str | None


def check_ascend_env_parity(
    toolkit_home: Path | None | object = _PARITY_SENTINEL,
) -> list[AscendVarCheck]:
    """Check Ascend env variable parity against the set_env.sh baseline.

    Audits each variable in :data:`ASCEND_CRITICAL_VARS`, comparing the
    live ``os.environ`` value against what the discovered toolkit layout
    expects.  No environment mutation is performed.

    Args:
        toolkit_home: Override the detected toolkit home.  Pass ``None``
            explicitly to return ``"no-toolkit"`` results without
            auto-discovery.  Omit (default sentinel) to trigger
            auto-discovery via :func:`_iter_ascend_toolkit_homes`.

    Returns:
        One :class:`AscendVarCheck` per variable in
        :data:`ASCEND_CRITICAL_VARS`.
    """
    if toolkit_home is _PARITY_SENTINEL:
        homes = _iter_ascend_toolkit_homes()
        toolkit_home = homes[0] if homes else None

    # Cast for type narrowing after sentinel check
    resolved_home: Path | None = toolkit_home  # type: ignore[assignment]

    if resolved_home is None:
        return [
            AscendVarCheck(
                name=name,
                current=os.environ.get(name),
                expected=None,
                status="no-toolkit",
                repair=None,
            )
            for name in ASCEND_CRITICAL_VARS
        ]

    tk = resolved_home

    def _dir_or_none(p: Path) -> str | None:
        return str(p) if p.is_dir() else None

    def _expected_aicpu() -> str:
        for candidate in (
            tk / "aicpu_kernels",
            tk / "aarch64-linux" / "lib64" / "plugin" / "opskernel",
            tk / "x86_64-linux" / "lib64" / "plugin" / "opskernel",
        ):
            if candidate.is_dir():
                return str(candidate)
        return str(tk)  # last-resort fallback: toolkit root itself

    plugin_home = _discover_ascend_plugin_home()

    # Build expected LD_LIBRARY_PATH segments (existing dirs only)
    ld_expected_dirs: list[Path] = [
        tk / "tools" / "aml" / "lib64",
        tk / "tools" / "aml" / "lib64" / "plugin",
        tk / "lib64" / "plugin" / "opskernel",
        tk / "lib64" / "plugin" / "nnengine",
        tk
        / "opp"
        / "built-in"
        / "op_impl"
        / "ai_core"
        / "tbe"
        / "op_tiling"
        / "lib"
        / "linux"
        / "aarch64",
        Path("/usr/local/Ascend/driver/lib64"),
        Path("/usr/local/Ascend/driver/lib64/common"),
        Path("/usr/local/Ascend/driver/lib64/driver"),
        tk / "lib64",
        tk / "runtime" / "lib64",
        tk / "fwkacllib" / "lib64",
        tk / "hccl" / "lib64",
    ]
    ld_expected_dirs.extend(_iter_ascend_arch_lib_dirs(tk))
    ld_expected_parts = [str(p) for p in ld_expected_dirs if p.is_dir()]

    # Build expected PYTHONPATH segments (existing dirs only)
    py_candidates: list[Path] = [
        tk / "python" / "site-packages",
        tk / "toolkit" / "python" / "site-packages",
        tk / "compiler" / "python" / "site-packages",
        tk / "hccl" / "python" / "site-packages",
        tk / "opp" / "built-in" / "op_impl" / "ai_core" / "tbe",
        tk / "opp" / "built-in" / "op_impl" / "vector_core" / "tbe",
    ]
    if plugin_home:
        py_candidates.append(plugin_home / "python" / "site-packages")
    python_expected_parts = [str(p) for p in py_candidates if p.is_dir()]

    # Map: var_name -> (expected_value_or_None, is_optional)
    expected_map: dict[str, tuple[str | None, bool]] = {
        "ASCEND_HOME": (str(tk), False),
        "ASCEND_TOOLKIT_HOME": (str(tk), False),
        "ASCEND_RUNTIME_PATH": (_dir_or_none(tk / "runtime"), False),
        "ASCEND_OPP_PATH": (_dir_or_none(tk / "opp"), False),
        "ASCEND_AICPU_PATH": (_expected_aicpu(), False),
        "ASCEND_PLUGIN_HOME": (str(plugin_home) if plugin_home else None, True),
        "PYTHONPATH": (
            os.pathsep.join(python_expected_parts) if python_expected_parts else None,
            True,
        ),
        "LD_LIBRARY_PATH": (
            os.pathsep.join(ld_expected_parts) if ld_expected_parts else None,
            False,
        ),
    }

    checks: list[AscendVarCheck] = []

    for var_name in ASCEND_CRITICAL_VARS:
        current = os.environ.get(var_name)
        expected, is_optional = expected_map[var_name]

        if var_name in ("PYTHONPATH", "LD_LIBRARY_PATH"):
            # PATH-like: verify that each expected segment appears in current
            if expected is None:
                checks.append(
                    AscendVarCheck(
                        name=var_name,
                        current=current,
                        expected=None,
                        status="ok",
                        repair=None,
                    )
                )
                continue

            current_parts: set[str] = set((current or "").split(os.pathsep)) - {""}
            expected_parts_list = [p for p in expected.split(os.pathsep) if p]
            missing = [p for p in expected_parts_list if p not in current_parts]

            if not missing:
                status = "ok"
                repair = None
            elif current is None:
                status = "optional-absent" if is_optional else "missing"
                repair = f'export {var_name}="{os.pathsep.join(missing)}"'
            else:
                status = "abnormal"
                repair = f'export {var_name}="{os.pathsep.join(missing)}{os.pathsep}{current}"'
        else:
            # Direct path: exact equality check
            if expected is None:
                status = "optional-absent" if current is None else "ok"
                repair = None
            elif current is None:
                status = "optional-absent" if is_optional else "missing"
                repair = None if is_optional else f'export {var_name}="{expected}"'
            elif current.rstrip("/") == expected.rstrip("/"):
                status = "ok"
                repair = None
            else:
                status = "abnormal"
                repair = f'export {var_name}="{expected}"'

        checks.append(
            AscendVarCheck(
                name=var_name,
                current=current,
                expected=expected,
                status=status,
                repair=repair,
            )
        )

    return checks


class NetworkDetector:
    """Detect and manage HuggingFace Hub connectivity."""

    HF_OFFICIAL = "https://huggingface.co"
    HF_MIRROR = "https://hf-mirror.com"
    TIMEOUT = 5  # seconds

    @staticmethod
    def _check_connectivity_via_urllib(url: str, timeout: int) -> bool:
        """Check endpoint reachability via stdlib HTTP request.

        Uses GET with a tiny byte range to validate real HTTP-level reachability
        while minimizing payload size.
        """
        req = urllib_request.Request(  # noqa: S310
            url=url,
            headers={"Range": "bytes=0-0", "User-Agent": "sagellm/1.0"},
            method="GET",
        )
        try:
            with urllib_request.urlopen(req, timeout=timeout) as resp:
                return 200 <= resp.status < 400
        except urllib_error.HTTPError as exc:
            return 200 <= exc.code < 400
        except (urllib_error.URLError, TimeoutError, ValueError):
            return False

    @staticmethod
    def check_connectivity(url: str, timeout: int = TIMEOUT) -> bool:
        """Check if a URL is reachable at HTTP layer.

        Args:
            url: URL to check
            timeout: Timeout in seconds

        Returns:
            True if URL is reachable, False otherwise
        """
        if NetworkDetector._check_connectivity_via_urllib(url, timeout):
            return True

        try:
            cmd = ["curl", "-s", "-L", "-I", "--max-time", str(timeout), url]
            result = subprocess.run(
                cmd,
                capture_output=True,
                timeout=timeout + 2,  # Give extra time for curl to finish
            )

            # Check if curl succeeded (exit code 0)
            if result.returncode == 0:
                # Parse HTTP status from response
                response_text = result.stdout.decode("utf-8", errors="ignore")
                # Look for HTTP status line like "HTTP/1.1 200 OK"
                for line in response_text.split("\n"):
                    if line.startswith("HTTP/"):
                        # Extract status code
                        parts = line.split()
                        if len(parts) >= 2:
                            status_code = parts[1]
                            # Accept 2xx and 3xx responses
                            return status_code.startswith("2") or status_code.startswith("3")
            return False
        except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
            return False

    @classmethod
    def detect_hf_endpoint(cls, verbose: bool = False) -> tuple[str, bool]:
        """Detect the best HuggingFace endpoint to use.

        Strategy:
        1. Check if HF_ENDPOINT already set (user preference)
        2. Try official HF endpoint (5s timeout)
        3. Fall back to mirror if official is unavailable

        Args:
            verbose: Print detection details

        Returns:
            Tuple of (endpoint_url, is_official)
        """
        # Check for existing user preference
        if existing_endpoint := os.environ.get("HF_ENDPOINT"):
            if verbose:
                print(f"✅ Using existing HF_ENDPOINT: {existing_endpoint}")
            return existing_endpoint, existing_endpoint == cls.HF_OFFICIAL

        # Try official endpoint
        if verbose:
            print(f"🔍 Testing HuggingFace official endpoint (timeout: {cls.TIMEOUT}s)...")

        if cls.check_connectivity(cls.HF_OFFICIAL, timeout=cls.TIMEOUT):
            if verbose:
                print(f"✅ Official endpoint reachable: {cls.HF_OFFICIAL}")
            return cls.HF_OFFICIAL, True

        if verbose:
            print("⚠️  Official endpoint unreachable, testing mirror...")

        if cls.check_connectivity(cls.HF_MIRROR, timeout=cls.TIMEOUT):
            if verbose:
                print(f"🪞 Mirror endpoint reachable: {cls.HF_MIRROR}")
            return cls.HF_MIRROR, False

        raise RuntimeError(
            "Unable to reach HuggingFace endpoints. "
            "Neither https://huggingface.co nor https://hf-mirror.com is reachable. "
            "Please check network/proxy settings, or set HF_ENDPOINT manually before inference."
        )


class EnvInitializer:
    """Initialize software-level environment variables."""

    # Internal marker: set when HF_ENDPOINT was auto-configured by us (not user)
    _HF_AUTOSET_MARKER = "_SAGELLM_HF_AUTOSET"

    @staticmethod
    def init_ascend_pythonpath() -> None:
        """Initialize Ascend Python-related paths when Ascend hardware exists.

        This ensures CANN Python modules (e.g., `tbe`) are importable without
        requiring users to source set_env.sh manually in every shell.
        """
        if shutil.which("npu-smi") is None:
            return

        toolkit_homes = _iter_ascend_toolkit_homes()
        if not toolkit_homes:
            return

        toolkit_home = toolkit_homes[0]
        plugin_home = _discover_ascend_plugin_home()

        if not os.environ.get("ASCEND_HOME"):
            os.environ["ASCEND_HOME"] = str(toolkit_home)
        if not os.environ.get("ASCEND_HOME_PATH"):
            os.environ["ASCEND_HOME_PATH"] = str(toolkit_home)
        if not os.environ.get("ASCEND_TOOLKIT_HOME"):
            os.environ["ASCEND_TOOLKIT_HOME"] = str(toolkit_home)
        if plugin_home is not None and not os.environ.get("ASCEND_PLUGIN_HOME"):
            os.environ["ASCEND_PLUGIN_HOME"] = str(plugin_home)

        runtime_path = toolkit_home / "runtime"
        opp_path = toolkit_home / "opp"
        aicpu_path = toolkit_home / "aicpu_kernels"
        if runtime_path.is_dir() and not os.environ.get("ASCEND_RUNTIME_PATH"):
            os.environ["ASCEND_RUNTIME_PATH"] = str(runtime_path)
        if opp_path.is_dir() and not os.environ.get("ASCEND_OPP_PATH"):
            os.environ["ASCEND_OPP_PATH"] = str(opp_path)
        if not os.environ.get("ASCEND_AICPU_PATH"):
            if aicpu_path.is_dir():
                os.environ["ASCEND_AICPU_PATH"] = str(aicpu_path)
            else:
                os.environ["ASCEND_AICPU_PATH"] = str(toolkit_home)

        ld_candidates = [
            toolkit_home / "tools" / "aml" / "lib64",
            toolkit_home / "tools" / "aml" / "lib64" / "plugin",
            toolkit_home / "lib64" / "plugin" / "opskernel",
            toolkit_home / "lib64" / "plugin" / "nnengine",
            toolkit_home
            / "opp"
            / "built-in"
            / "op_impl"
            / "ai_core"
            / "tbe"
            / "op_tiling"
            / "lib"
            / "linux"
            / "aarch64",
            Path("/usr/local/Ascend/driver/lib64"),
            Path("/usr/local/Ascend/driver/lib64/common"),
            Path("/usr/local/Ascend/driver/lib64/driver"),
            toolkit_home / "lib64",
            runtime_path / "lib64",
            toolkit_home / "fwkacllib" / "lib64",
            toolkit_home / "hccl" / "lib64",
        ]
        ld_candidates.extend(_iter_ascend_arch_lib_dirs(toolkit_home))
        os.environ["LD_LIBRARY_PATH"] = _prepend_existing_path(
            os.environ.get("LD_LIBRARY_PATH", ""), ld_candidates
        )
        if os.environ.get("SAGELLM_ASCEND_PRELOAD_LIBS", "0") == "1":
            _preload_ascend_shared_libraries(toolkit_home)

        python_candidates = [
            toolkit_home / "python" / "site-packages",
            toolkit_home / "toolkit" / "python" / "site-packages",
            toolkit_home / "compiler" / "python" / "site-packages",
            toolkit_home / "hccl" / "python" / "site-packages",
            toolkit_home / "opp" / "built-in" / "op_impl" / "ai_core" / "tbe",
            toolkit_home / "opp" / "built-in" / "op_impl" / "vector_core" / "tbe",
        ]
        if plugin_home is not None:
            python_candidates.append(plugin_home / "python" / "site-packages")

        if not any((candidate / "tbe").is_dir() for candidate in python_candidates):
            return

        os.environ["PYTHONPATH"] = _prepend_existing_path(
            os.environ.get("PYTHONPATH", ""), python_candidates
        )

        # PYTHONPATH updates after interpreter startup do not affect import
        # resolution automatically. Keep sys.path in sync for current process.
        for candidate in reversed(python_candidates):
            candidate_str = str(candidate)
            if not candidate.is_dir():
                continue
            if candidate_str in sys.path:
                sys.path.remove(candidate_str)
            sys.path.insert(0, candidate_str)

    @staticmethod
    def _set_hf_endpoint(endpoint: str) -> None:
        """Set HF_ENDPOINT and mark it as auto-configured (not user-set)."""
        os.environ["HF_ENDPOINT"] = endpoint
        os.environ[EnvInitializer._HF_AUTOSET_MARKER] = "1"

    @staticmethod
    def init_hf_endpoint(force_detect: bool = False, verbose: bool = False) -> None:
        """Initialize HF_ENDPOINT environment variable.

        Args:
            force_detect: Force fresh network detection (used by download commands)
            verbose: Print detection details

        Strategy:
        - If HF_ENDPOINT is set by the USER (not by us), always keep it.
        - If force_detect=False (default, fast startup path):
          - Load cached env_profile.json and use whatever endpoint is stored.
            NO network calls — startup must be instant even for --help.
          - If no cache, do full detection (first run only).
        - If force_detect=True (used by download commands like `hello`/`run`):
          - If user explicitly set HF_ENDPOINT, respect it.
          - If we auto-set it (from stale cache), do a fresh network probe and
            update. This handles VPN coming online after cache was written.
        """
        user_set = os.environ.get("HF_ENDPOINT") and not os.environ.get(
            EnvInitializer._HF_AUTOSET_MARKER
        )

        # Always respect user-explicitly-set HF_ENDPOINT
        if user_set:
            return

        # force_detect=True: clear our auto-set value and do a fresh probe
        if force_detect:
            os.environ.pop("HF_ENDPOINT", None)
            os.environ.pop(EnvInitializer._HF_AUTOSET_MARKER, None)
            endpoint, is_official = NetworkDetector.detect_hf_endpoint(verbose=verbose)
            EnvInitializer._set_hf_endpoint(endpoint)
            if verbose:
                tag = "✅ official" if is_official else "⚠️  mirror"
                print(f"{tag}: HF_ENDPOINT={endpoint}")
            return

        # Fast path: load from cache — no network calls at startup
        if not os.environ.get("HF_ENDPOINT"):
            cached = EnvInitializer.load_env_profile()
            if cached:
                cached_endpoint = (
                    cached.get("environment", {}).get("Network", {}).get("HF_ENDPOINT")
                )
                if cached_endpoint and cached_endpoint != "Not set":
                    EnvInitializer._set_hf_endpoint(cached_endpoint)
                    return

            # No cache — first run, do full network detection once
            try:
                endpoint, is_official = NetworkDetector.detect_hf_endpoint(verbose=verbose)
                EnvInitializer._set_hf_endpoint(endpoint)
                if verbose and endpoint:
                    if is_official:
                        print(f"✅ HF_ENDPOINT set to official: {endpoint}")
                    else:
                        print(f"⚠️  HF_ENDPOINT fallback to mirror: {endpoint}")
            except RuntimeError as exc:
                # Do not fail CLI startup path (e.g. `sagellm --help`),
                # inference commands use force_detect=True and will fail fast.
                if verbose:
                    print(f"⚠️  HF endpoint auto-detection skipped: {exc}")

    @staticmethod
    def init_hf_cache_dir() -> None:
        """Ensure HF_HOME is set to a valid cache directory."""
        if os.environ.get("HF_HOME"):
            return

        # Use ~/.cache/huggingface by default
        hf_home = Path.home() / ".cache" / "huggingface"
        hf_home.mkdir(parents=True, exist_ok=True)
        os.environ["HF_HOME"] = str(hf_home)

    @staticmethod
    def init_model_cache_dir() -> None:
        """Ensure sageLLM model cache directory exists."""
        # Use ~/.cache/sagellm by default
        cache_dir = Path.home() / ".cache" / "sagellm"
        cache_dir.mkdir(parents=True, exist_ok=True)
        os.environ["SAGELLM_CACHE_DIR"] = str(cache_dir)

    @staticmethod
    def init_logging_level() -> None:
        """Initialize logging level from environment or default to WARNING.

        Silences noisy third-party library loggers (transformers, torch, etc.)
        so that ``sagellm hello`` / inference commands don't flood the terminal.
        Pass SAGELLM_DEBUG=1 to opt into verbose output.
        """
        debug_mode = os.environ.get("SAGELLM_DEBUG") == "1"
        level = logging.DEBUG if debug_mode else logging.WARNING

        if not os.environ.get("LOGGING_LEVEL"):
            os.environ["LOGGING_LEVEL"] = "DEBUG" if debug_mode else "WARNING"

        # Actually apply to Python's logging system
        logging.basicConfig(level=level)

        # Silence noisy third-party library loggers unless in debug mode
        if not debug_mode:
            # Set env vars read by libraries at *first import* time — this ensures
            # suppression works even when the library is imported after this call.
            os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
            os.environ.setdefault("HF_HUB_VERBOSITY", "error")
            os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")

            _noisy = [
                "transformers",
                "transformers.modeling_utils",
                "transformers.tokenization_utils_base",
                "torch",
                "huggingface_hub",
                "huggingface_hub.file_download",
                "filelock",
                "accelerate",
                "safetensors",
                "urllib3",
            ]
            for _name in _noisy:
                logging.getLogger(_name).setLevel(logging.ERROR)

            # If transformers is already imported, apply via its own API too
            # (its logging module may have overridden our basicConfig setting).
            # IMPORTANT: do NOT import transformers here unconditionally —
            # that would add ~2 s to every CLI invocation including --help.
            if "transformers" in sys.modules:
                try:
                    import transformers.utils.logging as _tf_log

                    _tf_log.set_verbosity_error()
                    _tf_log.disable_progress_bar()
                except Exception:
                    pass
            if "huggingface_hub" in sys.modules:
                try:
                    logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
                except Exception:
                    pass

    @staticmethod
    def init_all(verbose: bool = False, force_detect: bool = False) -> dict[str, str]:
        """Initialize all software-level environment variables.

        Args:
            verbose: Print initialization details
            force_detect: Force network detection (slow, ~2s timeout)

        Returns:
            Dictionary of initialized environment variables
        """
        env_snapshot = {}

        # Initialize each component (use cache by default, fast)
        EnvInitializer.init_hf_endpoint(force_detect=force_detect, verbose=verbose)
        env_snapshot["HF_ENDPOINT"] = os.environ.get("HF_ENDPOINT", "")

        EnvInitializer.init_hf_cache_dir()
        env_snapshot["HF_HOME"] = os.environ.get("HF_HOME", "")

        EnvInitializer.init_model_cache_dir()
        env_snapshot["SAGELLM_CACHE_DIR"] = os.environ.get("SAGELLM_CACHE_DIR", "")

        EnvInitializer.init_ascend_pythonpath()
        env_snapshot["PYTHONPATH"] = os.environ.get("PYTHONPATH", "")

        EnvInitializer.init_logging_level()
        env_snapshot["LOGGING_LEVEL"] = os.environ.get("LOGGING_LEVEL", "")

        # Save to profile file only if force_detect was used
        if force_detect:
            EnvInitializer.save_env_profile()

        if verbose:
            print("✅ Software environment initialized")

        return env_snapshot

    @staticmethod
    def get_env_info() -> dict[str, dict[str, str]]:
        """Get current software environment info for display.

        Returns:
            Dictionary with environment variable groups and their values
        """
        return {
            "Network": {
                "HF_ENDPOINT": os.environ.get("HF_ENDPOINT", "Not set"),
            },
            "Cache": {
                "HF_HOME": os.environ.get("HF_HOME", "Not set"),
                "SAGELLM_CACHE_DIR": os.environ.get("SAGELLM_CACHE_DIR", "Not set"),
            },
            "Runtime": {
                "PYTHONPATH": os.environ.get("PYTHONPATH", "Not set"),
                "ASCEND_RUNTIME_PATH": os.environ.get("ASCEND_RUNTIME_PATH", "Not set"),
                "ASCEND_OPP_PATH": os.environ.get("ASCEND_OPP_PATH", "Not set"),
            },
            "Logging": {
                "LOGGING_LEVEL": os.environ.get("LOGGING_LEVEL", "Not set"),
            },
        }

    @staticmethod
    def get_env_profile_path() -> Path:
        """Get the path to the environment profile JSON file."""
        return Path.home() / ".sagellm" / "env_profile.json"

    @staticmethod
    def save_env_profile() -> None:
        """Save current environment configuration to JSON file."""
        import json
        from datetime import datetime

        env_info = EnvInitializer.get_env_info()
        profile_path = EnvInitializer.get_env_profile_path()

        # Ensure directory exists
        profile_path.parent.mkdir(parents=True, exist_ok=True)

        # Add metadata
        profile_data = {
            "version": "1.0",
            "updated_at": datetime.now().isoformat(),
            "environment": env_info,
        }

        with open(profile_path, "w", encoding="utf-8") as f:
            json.dump(profile_data, f, indent=2, ensure_ascii=False)

    @staticmethod
    def load_env_profile() -> dict | None:
        """Load environment configuration from JSON file.

        Returns:
            Dictionary with environment config or None if not found
        """
        import json

        profile_path = EnvInitializer.get_env_profile_path()
        if not profile_path.exists():
            return None

        try:
            with open(profile_path, encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError):
            return None
