"""Runtime environment helpers for prompt context."""

from __future__ import annotations

from dataclasses import dataclass
import importlib.metadata as importlib_metadata
import json
import os
from pathlib import Path
import platform
import shutil
import socket
import sys


PACKAGE_NAME = "memnixa"


@dataclass(slots=True, frozen=True)
class RuntimeExecutionInfo:
    """Describe how the current process was launched."""

    runtime_mode: str
    install_mode: str
    package_root: str
    project_root: str | None = None
    dist_info_path: str | None = None


def detect_host_name() -> str:
    """Return a stable host display name for prompt context."""
    for candidate in (platform.node(), socket.gethostname()):
        value = candidate.strip()
        if value:
            return value
    return "unknown"


def detect_shell_name() -> str | None:
    """Return the current shell name when it can be inferred."""
    if os.name == "nt":
        comspec = os.environ.get("COMSPEC", "").strip()
        if comspec:
            return Path(comspec).stem
        return "powershell"

    shell = os.environ.get("SHELL", "").strip()
    if shell:
        return Path(shell).name
    return None


def detect_runtime_execution(
    *,
    entrypoint_path: str | None = None,
    python_executable: str | None = None,
    package_root: Path | None = None,
    distribution_name: str = PACKAGE_NAME,
) -> RuntimeExecutionInfo:
    """Classify the current process as development or installed."""
    package_dir = (package_root or Path(__file__).resolve().parent).resolve()
    project_root = _find_project_root(package_dir)
    entrypoint = _normalize_path(entrypoint_path) or _normalize_path(
        resolve_entrypoint_path()
    )
    python_path = _normalize_path(python_executable) or _normalize_path(
        resolve_python_executable()
    )
    dist_info_path, direct_url = _load_distribution_metadata(distribution_name)
    editable_install = bool(
        isinstance(direct_url, dict)
        and direct_url.get("dir_info", {}).get("editable") is True
    )

    if project_root and _looks_like_development_launch(
        project_root=project_root,
        entrypoint_path=entrypoint,
        python_executable=python_path,
        package_root=package_dir,
    ):
        return RuntimeExecutionInfo(
            runtime_mode="development",
            install_mode="source_tree",
            package_root=str(package_dir),
            project_root=str(project_root),
            dist_info_path=dist_info_path,
        )

    if editable_install:
        return RuntimeExecutionInfo(
            runtime_mode="installed",
            install_mode="editable",
            package_root=str(package_dir),
            project_root=str(project_root) if project_root else None,
            dist_info_path=dist_info_path,
        )

    if dist_info_path:
        return RuntimeExecutionInfo(
            runtime_mode="installed",
            install_mode="standard",
            package_root=str(package_dir),
            project_root=str(project_root) if project_root else None,
            dist_info_path=dist_info_path,
        )

    return RuntimeExecutionInfo(
        runtime_mode="unknown",
        install_mode="unknown",
        package_root=str(package_dir),
        project_root=str(project_root) if project_root else None,
        dist_info_path=None,
    )


def resolve_entrypoint_path(argv: list[str] | None = None) -> str | None:
    """Resolve the launched command path when available."""
    argv = argv or sys.argv
    if not argv:
        return None
    raw = (argv[0] or "").strip()
    if not raw:
        return None

    candidate = Path(raw).expanduser()
    if candidate.is_absolute() and candidate.exists():
        return str(candidate.resolve())

    if raw.startswith("."):
        relative = (Path.cwd() / candidate).resolve()
        if relative.exists():
            return str(relative)

    discovered = shutil.which(raw)
    if discovered:
        return str(Path(discovered).resolve())

    if candidate.exists():
        return str(candidate.resolve())
    return str(candidate)


def resolve_launch_cwd() -> str:
    """Return the current launch directory."""
    return str(Path.cwd().resolve())


def resolve_python_executable() -> str:
    """Return the active Python executable path."""
    return str(Path(sys.executable).resolve())


def _normalize_path(value: str | Path | None) -> Path | None:
    if value is None:
        return None
    raw = str(value).strip()
    if not raw:
        return None
    return Path(raw).expanduser().resolve(strict=False)


def _find_project_root(start: Path) -> Path | None:
    for candidate in (start, *start.parents):
        if (candidate / "pyproject.toml").exists():
            return candidate.resolve()
    return None


def _load_distribution_metadata(
    distribution_name: str,
) -> tuple[str | None, dict[str, object] | None]:
    try:
        distribution = importlib_metadata.distribution(distribution_name)
    except importlib_metadata.PackageNotFoundError:
        return None, None

    dist_info_path = str(Path(distribution._path).resolve())  # type: ignore[attr-defined]
    direct_url_text = distribution.read_text("direct_url.json")
    if not direct_url_text:
        return dist_info_path, None
    try:
        payload = json.loads(direct_url_text)
    except json.JSONDecodeError:
        return dist_info_path, None
    if not isinstance(payload, dict):
        return dist_info_path, None
    return dist_info_path, payload


def _looks_like_development_launch(
    *,
    project_root: Path,
    entrypoint_path: Path | None,
    python_executable: Path | None,
    package_root: Path,
) -> bool:
    project_venv = project_root / ".venv"
    if python_executable and _is_relative_to(python_executable, project_venv):
        return True
    if entrypoint_path and (
        _is_relative_to(entrypoint_path, project_venv)
        or _is_relative_to(entrypoint_path, project_root / "src")
        or _is_relative_to(entrypoint_path, package_root)
    ):
        return True
    return False


def _is_relative_to(path: Path, base: Path) -> bool:
    try:
        path.relative_to(base)
        return True
    except ValueError:
        return False
