"""Egress-proxy (iron-proxy) plumbing for the Docker backend.

Builds the mount/env/host args that route a sandbox through the host-side
credential firewall, and guards the three config surfaces (docker_forward_env,
docker_env, docker_extra_args) that could otherwise weaken or bypass it.
"""

from __future__ import annotations

import hashlib
import json
import logging

logger = logging.getLogger("tools.environments.docker")

_EGRESS_LABEL_KEY = "hermes-egress"
_CONTAINER_CA = "/etc/ssl/certs/hermes-egress-ca.crt"
_NODE_OPTIONS_SENTINEL = "_HERMES_EGRESS_NODE_OPTIONS_APPEND"
_CA_MODE_FLAGS = {"--use-openssl-ca", "--use-bundled-ca"}

# Env names whose override would weaken or bypass enforced egress.
_PROXY_CONTROL_ENV = frozenset({
    "HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy",
    "NO_PROXY", "no_proxy",
    "REQUESTS_CA_BUNDLE", "SSL_CERT_FILE", "CURL_CA_BUNDLE",
    "NODE_EXTRA_CA_CERTS"})


def _egress_proxy_args_for_docker() -> tuple[list[str], dict[str, str], list[str]]:
    """``(volume_args, env_overrides, host_args)`` for routing through iron-proxy; all empty
    when the proxy is disabled/unconfigured/not running. Under ``proxy.enforce_on_docker``
    (default) any half-configured state raises so the sandbox refuses to start unprotected;
    otherwise it warns and continues. Only ImportError is swallowed — a broken config must
    fail visibly rather than silently disable enforcement."""
    try:
        from hermes_cli.config import load_config
        from agent.proxy_sources import iron_proxy as ip
    except ImportError as exc:
        logger.debug("Egress proxy plumbing unavailable: %s", exc)
        return ([], {}, [])

    proxy_cfg = load_config().get("proxy") or {}
    if not proxy_cfg.get("enabled"):
        return ([], {}, [])

    status = ip.get_status()
    enforce = bool(proxy_cfg.get("enforce_on_docker", True))

    def _degraded(msg: str):
        if enforce:
            raise RuntimeError(msg)
        logger.warning("%s — continuing without proxy (enforce_on_docker=false).", msg)
        return ([], {}, [])

    if not status.configured:
        return _degraded(
            "proxy.enabled is true but iron-proxy is not configured. "
            "Run `hermes egress setup` to mint tokens and write proxy.yaml.")
    if not (status.pid and status.listening):
        return _degraded(
            f"iron-proxy is enabled but not running on port {status.tunnel_port}. "
            "Start it with `hermes egress start`.")
    if status.ca_cert_path is None or not status.ca_cert_path.exists():
        # Configured a moment ago but the trust anchor vanished: proxy env vars
        # without the CA would make every TLS handshake fail.
        return _degraded(
            f"iron-proxy CA cert vanished from {status.ca_cert_path}. "
            "Re-run `hermes egress setup` to regenerate it.")
    # Empty/corrupt mappings look like an upstream outage from inside the
    # sandbox (every request 403s); refuse rather than ship a broken sandbox.
    mappings = ip.load_mappings()
    if not mappings:
        return _degraded(
            "iron-proxy is configured but mappings.json is empty or "
            "corrupt.  Re-run `hermes egress setup` to mint provider "
            "tokens before starting a sandbox.")

    volume_args = ["-v", f"{status.ca_cert_path}:{_CONTAINER_CA}:ro"]

    # tunnel_port serves CONNECT (HTTPS); the plain-HTTP forward listener is on +1.
    proxy_url = f"http://host.docker.internal:{status.tunnel_port}"
    plain_http_url = f"http://host.docker.internal:{status.tunnel_port + 1}"
    env_overrides: dict[str, str] = {
        # Both casings: some tools only read one.
        "HTTPS_PROXY": proxy_url,
        "https_proxy": proxy_url,
        "HTTP_PROXY": plain_http_url,
        "http_proxy": plain_http_url,
        # Loopback-only so in-sandbox dev servers/local LLMs bypass the proxy.
        "NO_PROXY": "127.0.0.1,localhost,::1",
        "no_proxy": "127.0.0.1,localhost,::1",
        # CA bundles: Python/curl vars REPLACE the system store, NODE_EXTRA_CA_CERTS
        # only ADDS to it. NODE_OPTIONS=--use-openssl-ca narrows that asymmetry
        # but must be APPENDED to the operator's NODE_OPTIONS, not clobber it —
        # so it travels in a sentinel key that merge_egress_env() resolves.
        "REQUESTS_CA_BUNDLE": _CONTAINER_CA,
        "SSL_CERT_FILE": _CONTAINER_CA,
        "CURL_CA_BUNDLE": _CONTAINER_CA,
        "NODE_EXTRA_CA_CERTS": _CONTAINER_CA,
        "HERMES_EGRESS_PROXY": "1",  # lets the in-sandbox agent know it is proxy-aware
        _NODE_OPTIONS_SENTINEL: "--use-openssl-ca"}

    # Proxy tokens under the standard provider env names (and their aliases) so
    # SDKs work unchanged; HERMES_PROXY_TOKEN_* copies are for diagnostics.
    for m in mappings:
        env_overrides[m.real_env_name] = m.proxy_token
        env_overrides[f"HERMES_PROXY_TOKEN_{m.real_env_name}"] = m.proxy_token
        for alias in getattr(m, "alias_env_names", ()) or ():
            env_overrides[alias] = m.proxy_token

    # Linux needs an explicit host-gateway mapping; Docker Desktop already has it.
    host_args = ["--add-host", "host.docker.internal:host-gateway"]
    return (volume_args, env_overrides, host_args)


def _egress_reuse_fingerprint(
    volume_args: list[str], env_overrides: dict[str, str], host_args: list[str]) -> str:
    """Stable Docker-label value for the egress posture of a container."""
    if not (volume_args or env_overrides or host_args):
        return "off"
    payload = json.dumps(
        {"volume_args": volume_args, "env_overrides": env_overrides, "host_args": host_args},
        sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:24]


def _egress_enforce_on_docker(default: bool = True) -> bool:
    """Read proxy.enforce_on_docker; any config failure fails safe to *default*."""
    try:
        from hermes_cli.config import load_config
        return bool((load_config().get("proxy") or {}).get("enforce_on_docker", default))
    except (ImportError, OSError):
        return default
    except Exception as exc:  # malformed config.yaml etc.
        logger.warning("Could not read proxy config for egress collision check: %s", exc)
        return default


def _critical_egress_env_names(env_overrides: dict[str, str]) -> set[str]:
    """Env names that would weaken or bypass enforced egress if overridden.

    Every name the egress layer writes is critical: mapped tokens land under arbitrary
    ``real_env_name``/``alias_env_names`` entries from mappings.json, so a suffix
    heuristic would silently unprotect any mapped credential not ending in
    ``_API_KEY``/``_TOKEN`` (e.g. ``AWS_SECRET_ACCESS_KEY``)."""
    return set(_PROXY_CONTROL_ENV) | {"NODE_OPTIONS"} | set(env_overrides)


# ``docker run`` boolean shorthands that may precede a value-taking shorthand
# in a single-dash chain (pflag: "-iteNAME=v" parses as -i -t -e NAME=v).
_DOCKER_RUN_BOOL_SHORTHANDS = frozenset("ditPq")


def _env_flag_value(arg: str) -> tuple[str, str | None] | None:
    """``("env"|"env-file", inline_value)`` when ``arg`` spells a docker env flag
    under pflag parsing, else ``None``. ``inline_value`` is the joined value
    (``--env=N=v``, ``-eN=v``) or ``None`` when the flag consumes the next arg
    (``-e``, ``--env``, ``-ite``)."""
    if arg.startswith("--"):
        flag, sep, inline = arg.partition("=")
        if flag in ("--env", "--env-file"):
            return flag[2:], inline if sep else None
        return None
    if not arg.startswith("-"):
        return None  # positional (image/command), never a flag
    # A single dash heads a shorthand chain: every letter before the last must
    # be a boolean shorthand, and the last takes the rest of the arg (or the
    # next arg when bare) as its value.
    for pos, ch in enumerate(arg[1:]):
        if ch in _DOCKER_RUN_BOOL_SHORTHANDS:
            continue
        if ch == "e":
            rest = arg[pos + 2:]
            if rest.startswith("="):
                rest = rest[1:]
            return "env", rest or None
        return None
    return None


def _extra_args_egress_collisions(extra_args: list[str], critical_names: set[str]) -> list[str]:
    """Return docker_extra_args entries that can override egress controls."""
    # Folded membership: ``-e NAME`` resolves NAME in the host env, which is
    # case-insensitive on Windows — ``-eopenai_api_key`` would inject the real
    # credential past the token swap.
    critical_folded = {n.upper() for n in critical_names}
    collisions: list[str] = []
    i = 0
    while i < len(extra_args):
        arg = extra_args[i]
        # pflag stops flag parsing at "--"; the rest is image/command. Bare
        # positionals keep being scanned: a preceding long flag may consume
        # them as its value, and over-scanning errs toward enforcement.
        if arg == "--":
            break
        parsed = _env_flag_value(arg)
        if parsed is not None:
            kind, inline_value = parsed
            if kind == "env-file":
                collisions.append("--env-file")
            else:
                value = inline_value if inline_value is not None else (
                    extra_args[i + 1] if i + 1 < len(extra_args) else "")
                name = value.split("=", 1)[0]
                if name.upper() in critical_folded:
                    collisions.append(name)
            i += 1 if inline_value is not None else 2
            continue
        if arg.partition("=")[0] in ("--network", "--net"):
            collisions.append(arg)
        i += 1
    return sorted(set(collisions))


def _collision_guard(msg: str, *, enforce: bool, remedy: str, consequence: str) -> None:
    """Fail loud under enforcement, otherwise warn and let the user's config win."""
    msg = f"{msg}; enforce_on_docker is {'enabled' if enforce else 'disabled'}."
    if enforce:
        raise RuntimeError(f"{msg}  {remedy}")
    logger.warning("%s  %s", msg, consequence)


def check_forward_env_collisions(forward_env: list[str], critical: set[str], enforce: bool) -> None:
    # Folded membership: forward_env names are resolved via os.getenv() on the
    # host, which is case-insensitive on Windows — ``openai_api_key`` would
    # inject the real credential into the container under a variant name.
    critical_folded = {k.upper() for k in critical}
    collisions = sorted(k for k in forward_env if k.upper() in critical_folded)
    if collisions:
        _collision_guard(
            f"docker_forward_env would inject real egress-protected variables {collisions}",
            enforce=enforce,
            remedy="Remove these names from docker_forward_env or disable enforce_on_docker "
                   "to opt out of egress isolation.",
            consequence="Explicit docker_forward_env values will override egress tokens.")


def check_docker_env_collisions(user_env: dict[str, str], egress_env: dict[str, str], enforce: bool) -> None:
    """Reject docker_env overrides of proxy-control vars or real provider keys (read from the
    live token mappings; for those ANY override collides since a real key bypasses the swap)."""
    provider_keys: set[str] = set()
    try:
        from agent.proxy_sources import iron_proxy as ip
        provider_keys = {m.real_env_name for m in ip.load_mappings()}
    except Exception:  # best-effort
        pass

    def _collides(k: str) -> bool:
        if k not in user_env:
            return False
        if k in provider_keys:
            return k not in egress_env or user_env[k] != egress_env[k]
        return k in egress_env and user_env[k] != egress_env[k]

    collisions = sorted(k for k in (_PROXY_CONTROL_ENV | provider_keys) if _collides(k))
    if collisions:
        _collision_guard(
            f"docker_env in config.yaml overrides egress-proxy variables {collisions}",
            enforce=enforce,
            remedy="Remove these keys from docker_env or disable enforce_on_docker to opt out "
                   "of egress isolation.",
            consequence="Falling back to docker_env values; sandbox traffic will NOT route "
                        "through the proxy.")


def check_extra_args_collisions(extra_args: list[str], critical: set[str], enforce: bool) -> None:
    collisions = _extra_args_egress_collisions(extra_args, critical)
    if collisions:
        _collision_guard(
            f"docker_extra_args would override egress-proxy controls {collisions}",
            enforce=enforce,
            remedy="Remove these args or disable enforce_on_docker to opt out of egress isolation.",
            consequence="Extra Docker args may bypass egress isolation.")


def merge_egress_env(user_env: dict[str, str], egress_env: dict[str, str], enforce: bool) -> dict[str, str]:
    """Merge docker_env with egress overrides (egress wins under enforcement, docker_env
    otherwise) and resolve the NODE_OPTIONS sentinel: the flag is APPENDED to the operator's
    NODE_OPTIONS after stripping conflicting CA-mode flags so it wins deterministically."""
    merged = {**user_env, **egress_env} if enforce and egress_env else {**egress_env, **user_env}

    raw_append = merged.pop(_NODE_OPTIONS_SENTINEL, None)
    if raw_append:
        append_token = raw_append.strip()
        tokens = merged.get("NODE_OPTIONS", "").split()
        if append_token in _CA_MODE_FLAGS:
            dropped = [t for t in tokens if t in _CA_MODE_FLAGS and t != append_token]
            if dropped:
                logger.warning(
                    "Overriding conflicting NODE_OPTIONS CA-mode flag(s) %s "
                    "with egress-required %s to keep Node routed through the "
                    "egress CA store.", dropped, append_token)
            tokens = [t for t in tokens if t not in _CA_MODE_FLAGS or t == append_token]
        if append_token not in tokens:
            tokens.append(append_token)
        merged["NODE_OPTIONS"] = " ".join(tokens).strip()
        if not merged["NODE_OPTIONS"]:
            merged.pop("NODE_OPTIONS", None)
    return merged
