"""Background-mode process management.

``--output background``: the parent spens process validates, creates session
state, emits ``started``, then spawns the session loop as a detached child --
``subprocess.Popen`` of itself with ``--output jsonl`` plus
``--_reuse-session-id`` so the child appends to the same ``events.jsonl`` /
``state.json``.  The child is fully detached from the parent's terminal
(setsid on POSIX, a console-less detached process on Windows, stdio ->
DEVNULL -- its output lives in the session's ``events.jsonl``), so it survives
the parent exiting (or the terminal / SSH session closing).  The parent exits
0 immediately after the spawn, printing the session id to stdout.
"""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path

from spens import events as spens_events
from spens import runner


def _spawn_detached(child_cmd: list[str]) -> subprocess.Popen:
    """Spawn the session child so it outlives this process and its terminal.

    ``start_new_session=True`` is POSIX-only (setsid: new session, no
    controlling terminal); on Windows it is silently ignored, so detachment
    is done with creation flags instead: ``DETACHED_PROCESS`` gives the child
    no console at all (it never sees the parent terminal's Ctrl+C or close
    events) and ``CREATE_NEW_PROCESS_GROUP`` keeps it out of the parent's
    ctrl-c group.  ``CREATE_BREAKAWAY_FROM_JOB`` additionally escapes a job
    object that would kill the child with the parent, when the parent's job
    allows breakaway; if it does not, the spawn is retried without it.
    """
    stdio = {
        "stdin": subprocess.DEVNULL,
        "stdout": subprocess.DEVNULL,
        "stderr": subprocess.DEVNULL,
        "close_fds": True,
    }
    if sys.platform != "win32":
        return subprocess.Popen(child_cmd, start_new_session=True, **stdio)

    flags = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
    try:
        return subprocess.Popen(
            child_cmd, creationflags=flags | subprocess.CREATE_BREAKAWAY_FROM_JOB, **stdio
        )
    except OSError:
        return subprocess.Popen(child_cmd, creationflags=flags, **stdio)


def launch_background_session(
    env_name: str,
    agent_name: str,
    workspace: str | Path,
    *,
    prompt: str,
    accept_changes: bool,
    reject_changes: bool,
    spens_dir: str | Path | None = None,
    session_id: str | None = None,
    no_cache: bool = False,
) -> str:
    """Boot the session, spawn the detached child, return the session id.

    The parent's work ends at the ``started`` event; everything after it
    (builds, interceptor, agent, cleanup, summary) is the child's job.  The
    child reuses the already-created session id, so state ownership is
    unambiguous and both processes (plus ``spens cancel`` / ``status``)
    serialize on the same session-dir lock.
    """
    boot = runner._boot_session(
        env_name, agent_name, workspace,
        prompt=prompt,
        accept_changes=accept_changes,
        reject_changes=reject_changes,
        spens_dir=spens_dir,
        session_id=session_id,
        output_mode="background",
    )

    child_cmd = [
        sys.executable, "-m", "spens",
        env_name, agent_name, str(boot.workspace_path), prompt,
        "--output", "jsonl",
        "--spens-dir", str(boot.spens_root),
        "--_reuse-session-id", boot.session_id,
    ]
    # Validation guarantees exactly one of the two change flags.
    child_cmd.append("--accept-changes" if accept_changes else "--reject-changes")
    if no_cache:
        child_cmd.append("--rebuild")

    _spawn_detached(child_cmd)
    spens_events.active().close()
    return boot.session_id
