"""Environment information."""

import functools
import logging
import os
import platform
import subprocess
from typing import Dict, List, Optional, Union

from langsmith.utils import get_docker_compose_command
from langsmith.env._git import exec_git

try:
    # psutil is an optional dependency
    import psutil

    _PSUTIL_AVAILABLE = True
except ImportError:
    _PSUTIL_AVAILABLE = False
logger = logging.getLogger(__name__)


def get_runtime_and_metrics() -> dict:
    """Get the runtime information as well as metrics."""
    return {**get_runtime_environment(), **get_system_metrics()}


def get_system_metrics() -> Dict[str, Union[float, dict]]:
    """Get CPU and other performance metrics."""
    global _PSUTIL_AVAILABLE
    if not _PSUTIL_AVAILABLE:
        return {}
    try:
        process = psutil.Process(os.getpid())
        metrics: Dict[str, Union[float, dict]] = {}

        with process.oneshot():
            mem_info = process.memory_info()
            metrics["thread_count"] = float(process.num_threads())
            metrics["mem"] = {
                "rss": float(mem_info.rss),
            }
            ctx_switches = process.num_ctx_switches()
            cpu_times = process.cpu_times()
            metrics["cpu"] = {
                "time": {
                    "sys": cpu_times.system,
                    "user": cpu_times.user,
                },
                "ctx_switches": {
                    "voluntary": float(ctx_switches.voluntary),
                    "involuntary": float(ctx_switches.involuntary),
                },
                "percent": process.cpu_percent(),
            }
        return metrics
    except Exception as e:
        # If psutil is installed but not compatible with the build,
        # we'll just cease further attempts to use it.
        _PSUTIL_AVAILABLE = False
        logger.debug("Failed to get system metrics: %s", e)
        return {}


@functools.lru_cache(maxsize=1)
def get_runtime_environment() -> dict:
    """Get information about the environment."""
    # Lazy import to avoid circular imports
    from langsmith import __version__

    shas = get_release_shas()
    return {
        "sdk": "langsmith-py",
        "sdk_version": __version__,
        "library": "langsmith",
        "platform": platform.platform(),
        "runtime": "python",
        "py_implementation": platform.python_implementation(),
        "runtime_version": platform.python_version(),
        "langchain_version": get_langchain_environment(),
        "langchain_core_version": get_langchain_core_version(),
        **shas,
    }


@functools.lru_cache(maxsize=1)
def get_langchain_environment() -> Optional[str]:
    try:
        import langchain  # type: ignore

        return langchain.__version__
    except:  # noqa
        return None


@functools.lru_cache(maxsize=1)
def get_langchain_core_version() -> Optional[str]:
    try:
        import langchain_core  # type: ignore

        return langchain_core.__version__
    except ImportError:
        return None


@functools.lru_cache(maxsize=1)
def get_docker_version() -> Optional[str]:
    import subprocess

    try:
        docker_version = (
            subprocess.check_output(["docker", "--version"]).decode("utf-8").strip()
        )
    except FileNotFoundError:
        docker_version = "unknown"
    except:  # noqa
        return None
    return docker_version


@functools.lru_cache(maxsize=1)
def get_docker_compose_version() -> Optional[str]:
    try:
        docker_compose_version = (
            subprocess.check_output(["docker-compose", "--version"])
            .decode("utf-8")
            .strip()
        )
    except FileNotFoundError:
        docker_compose_version = "unknown"
    except:  # noqa
        return None
    return docker_compose_version


@functools.lru_cache(maxsize=1)
def _get_compose_command() -> Optional[List[str]]:
    try:
        compose_command = get_docker_compose_command()
    except ValueError as e:
        compose_command = [f"NOT INSTALLED: {e}"]
    except:  # noqa
        return None
    return compose_command


@functools.lru_cache(maxsize=1)
def get_docker_environment() -> dict:
    """Get information about the environment."""
    compose_command = _get_compose_command()
    return {
        "docker_version": get_docker_version(),
        "docker_compose_command": (
            " ".join(compose_command) if compose_command is not None else None
        ),
        "docker_compose_version": get_docker_compose_version(),
    }


def get_langchain_env_vars() -> dict:
    """Retrieve the langchain environment variables."""
    env_vars = {k: v for k, v in os.environ.items() if k.startswith("LANGCHAIN_")}
    for key in list(env_vars):
        if "key" in key.lower():
            v = env_vars[key]
            env_vars[key] = v[:2] + "*" * (len(v) - 4) + v[-2:]
    return env_vars


@functools.lru_cache(maxsize=1)
def get_langchain_env_var_metadata() -> dict:
    """Retrieve the langchain environment variables."""
    excluded = {
        "LANGCHAIN_API_KEY",
        "LANGCHAIN_ENDPOINT",
        "LANGCHAIN_TRACING_V2",
        "LANGCHAIN_PROJECT",
        "LANGCHAIN_SESSION",
        "LANGSMITH_RUNS_ENDPOINTS",
        # Control-plane signing secrets the substring filter misses; excluded here.
        "LANGSMITH_SIGNING_JWKS",
        "LANGSMITH_SANDBOX_CALLBACK_SIGNING_JWK",
    }
    langchain_metadata = {
        k: v
        for k, v in os.environ.items()
        if (k.startswith("LANGCHAIN_") or k.startswith("LANGSMITH_"))
        and k not in excluded
        and "key" not in k.lower()
        and "secret" not in k.lower()
        and "token" not in k.lower()
    }
    env_revision_id = langchain_metadata.pop("LANGCHAIN_REVISION_ID", None)
    if env_revision_id:
        langchain_metadata["revision_id"] = env_revision_id
    elif default_revision_id := _get_default_revision_id():
        langchain_metadata["revision_id"] = default_revision_id

    return langchain_metadata


@functools.lru_cache(maxsize=1)
def _get_default_revision_id() -> Optional[str]:
    """Get the default revision ID based on `git describe`."""
    try:
        return exec_git(["describe", "--tags", "--always", "--dirty"])
    except BaseException:
        return None


@functools.lru_cache(maxsize=1)
def get_release_shas() -> Dict[str, str]:
    common_release_envs = [
        "VERCEL_GIT_COMMIT_SHA",
        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA",
        "COMMIT_REF",
        "RENDER_GIT_COMMIT",
        "CI_COMMIT_SHA",
        "CIRCLE_SHA1",
        "CF_PAGES_COMMIT_SHA",
        "REACT_APP_GIT_SHA",
        "SOURCE_VERSION",
        "GITHUB_SHA",
        "TRAVIS_COMMIT",
        "GIT_COMMIT",
        "BUILD_VCS_NUMBER",
        "bamboo_planRepository_revision",
        "Build.SourceVersion",
        "BITBUCKET_COMMIT",
        "DRONE_COMMIT_SHA",
        "SEMAPHORE_GIT_SHA",
        "BUILDKITE_COMMIT",
    ]
    shas = {}
    for env in common_release_envs:
        env_var = os.environ.get(env)
        if env_var is not None:
            shas[env] = env_var
    return shas
