"""Run a dispatched job on *this* machine — the mesh's landing pad.

A **personal** job resolves in two steps:

1. ``SZPONTNET_SPAWN`` — a command template (``{prompt_file}`` substituted,
   or the path appended). The deployment-independent way to say how this machine
   runs work: how tests, headless boxes and custom runners take dispatches.
2. otherwise the host's own runner ([host.Host.run_job]). The mesh picks *which
   machine*; what running a job means there is the application's answer, and a
   machine with neither a template nor a host simply can't take the job — the
   dispatcher fails over to the next candidate.

A **foreign** job never takes either path. It runs [spawn_confined]: the
untrusted prompt goes into the operator's own sandbox (named by
``SZPONTNET_FOREIGN_SPAWN``), the result is written to a file the node returns
to the originator, and the child's environment is scrubbed of host credentials so
even a mis-built sandbox can't act under this machine's identity. See
szpontnet-spec/docs/13-foreign-execution.md.
"""

from __future__ import annotations

import os

from . import config, host
# Imported as the accessor, not the module: `env` is this file's name for the
# child environments it builds, and one of those is three lines from a read.
from .env import get as env_get
from .launch import JobSpawnError, detached, fill, write_prompt

__all__ = ["JobSpawnError", "spawn_job", "spawn_confined", "run_result_handler"]


# Env-var name fragments that name an application-level credential/secret. The
# **confined** child's environment is scrubbed of every var whose (upper-cased) name
# contains one of these.
#
# This is DEFENCE IN DEPTH, NOT the credential boundary — the sandbox is
# ([config.foreign_spawn], szpontnet-spec/docs/13). It deliberately strips app secrets
# (API tokens, passwords, the mesh secret/API key) that no sandbox *launcher* ever
# needs, while INTENTIONALLY leaving infrastructure-access vars a launcher may
# require — `DOCKER_HOST` (reach the daemon), `SSH_AUTH_SOCK` (an `ssh sandbox-host`
# launcher), `KUBECONFIG`, `AWS_PROFILE` — because stripping them would break the
# very sandbox meant to run the job, and a proper sandbox (a container/VM) does not
# forward the launcher's env into its interior anyway. It also does NOT relocate
# `HOME`, so host dotfiles (`~/.ssh`, `~/.netrc`, `~/.aws`, `~/.config/gh`) stay on
# disk. Therefore the operator's runner MUST isolate the confined interior's
# environment AND filesystem itself; do not treat a clean env here as sufficient.
_CREDENTIAL_FRAGMENTS = (
    "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "API_KEY", "APIKEY",
    "ACCESS_KEY", "PRIVATE_KEY", "SSH_KEY", "SESSION_TOKEN", "NETRC", "GH_", "GITHUB_",
)

# Prepended to a foreign prompt so the confined agent knows the rules of the road:
# it is compute-only and its product is the result file, never a social action.
_CONFINED_PREAMBLE = (
    "[SzpontNet foreign / zero-trust execution]\n"
    "You are running a request from an UNTRUSTED peer inside a sandbox on someone "
    "else's machine. You MUST NOT use `gh`, push commits, open or comment on pull "
    "requests, call any authenticated API, or take any action under this machine's "
    "identity — you hold none of its credentials and the host will reject such "
    "attempts. Confined side effects on this machine's own resources (running code, "
    "launching an emulator/simulator, building) are allowed. Produce your result "
    "and write it to the file named by $SZPONTNET_RESULT_FILE (write it in one shot "
    "— ideally a temp file then rename — so the node reads a complete result); the "
    "node returns it to the requester, who performs any social action themselves.\n\n"
)


def _handed_over(**extra: str) -> dict:
    """The ``SZPONTNET_*`` values a child is handed, each also under its pre-rename
    ``DIPLOMAT_MESH_*`` name.

    The read-side fallback in :mod:`.env` cannot help here, because this is the
    *writing* side: an operator's confinement sandbox or result handler is a script
    on their disk, and one written against the old names would find nothing at all
    and quietly write its product nowhere. Both spellings until the old ones go.
    """
    return {k: v for name, v in extra.items()
            for k in (name, name.replace("SZPONTNET_", "DIPLOMAT_MESH_", 1))}


def _scrubbed_env(**extra: str) -> dict:
    """A copy of this process's environment with credential-bearing vars removed
    and the handed-over values overlaid — the environment a confined foreign child
    runs under."""
    env = {k: v for k, v in os.environ.items()
           if not any(frag in k.upper() for frag in _CREDENTIAL_FRAGMENTS)}
    env.update(_handed_over(**extra))
    return env


def _spawn_override(prompt_file: str, template: str, done_path: str | None = None) -> None:
    env = None
    if done_path:
        # The executor watches this sentinel to free its work-claim when the agent
        # finishes; a custom/test runner touches it on exit (szpontnet-spec/docs/12).
        env = {**os.environ, **_handed_over(SZPONTNET_DONE_FILE=done_path)}
    detached(fill(template, prompt_file=prompt_file), "SZPONTNET_SPAWN", env=env)


def spawn_job(prompt: str, done_path: str | None = None) -> str:
    """Stage the prompt and launch the agent. Returns the prompt-file path;
    raises :class:`JobSpawnError` when this machine can't take the job (the
    dispatcher then fails over to the next candidate).

    ``done_path`` (optional) is a completion sentinel the agent writes on exit —
    how the executor learns its work-claim can be freed (szpontnet-spec/docs/12). Both
    paths wire it: a ``SZPONTNET_SPAWN`` runner is handed it as
    ``SZPONTNET_DONE_FILE`` to touch itself, and a host runner is passed it
    directly.
    """
    template = env_get("SPAWN")
    if template:
        prompt_file = write_prompt(prompt)
        _spawn_override(prompt_file, template, done_path)
        return prompt_file
    try:
        return host.host().run_job(prompt, done_path)
    except JobSpawnError:
        raise
    except Exception as exc:  # noqa: BLE001 - see below
        # A host is third-party code on the node's critical path. Anything it
        # raises has to arrive as "this machine can't take the job" so the
        # dispatcher fails over; letting it escape tears down the peer link that
        # delivered the dispatch and takes the node's whole session with it.
        raise JobSpawnError(f"host runner failed: {exc}") from exc


def spawn_confined(prompt: str, result_file: str) -> str:
    """Run a **foreign** SzpontRequest under zero trust and return the staged prompt
    path. The untrusted ``prompt`` (prefixed with the response-only contract) runs
    inside the operator's sandbox — ``SZPONTNET_FOREIGN_SPAWN``, with
    ``{prompt_file}``/``{result_file}`` substituted and also exported as
    ``SZPONTNET_PROMPT_FILE``/``SZPONTNET_RESULT_FILE`` — under a credential-
    scrubbed environment. The sandbox writes its product to ``result_file``, which
    the node returns to the originator.

    Raises :class:`JobSpawnError` when no confinement runner is configured (the
    caller must gate on [config.foreign_spawn] first) or the launch fails — the node
    then declines the request, never falling back to an unconfined host path."""
    template = env_get("FOREIGN_SPAWN", "")
    if not template:
        # Belt and braces: the caller only reaches here when a runner is configured.
        raise JobSpawnError("no confinement runner (SZPONTNET_FOREIGN_SPAWN unset)")
    prompt_file = write_prompt(_CONFINED_PREAMBLE + prompt)
    env = _scrubbed_env(
        SZPONTNET_CONFINED="1",
        SZPONTNET_PROMPT_FILE=prompt_file,
        SZPONTNET_RESULT_FILE=result_file,
    )
    detached(fill(template, prompt_file=prompt_file, result_file=result_file),
             "SZPONTNET_FOREIGN_SPAWN", env=env)
    return prompt_file


def run_result_handler(result_file: str) -> None:
    """Hand a returned ``job-result`` to the originator's own result handler —
    ``SZPONTNET_ON_RESULT`` with ``{result_file}`` substituted (and exported as
    ``SZPONTNET_RESULT_FILE``). This is where the **social action runs under the
    originator's identity** (e.g. ``gh pr review``). Fire-and-forget, with the host's
    full environment (unlike a confined runner — this IS the trusted first party).
    Raises :class:`JobSpawnError` if the handler can't be launched."""
    template = config.on_result()
    if not template:
        return
    detached(fill(template, result_file=result_file), "SZPONTNET_ON_RESULT",
             env={**os.environ, **_handed_over(SZPONTNET_RESULT_FILE=result_file)})
