"""Session lifecycle: pin-or-create an adw_id, build the Run object.

`ensure(cfg, adw_id)` joins the session if it exists or creates it under
exactly that id (pinned ids for repeatable runs); omitted, a fresh id is
minted and printed so the next ADW can pick it up.
"""

from __future__ import annotations

import os
import shlex
import signal
import sys
from pathlib import Path

from . import git_helper, notify, permissions, supervisor
from .control import read_proc_start_ticks
from .data_types import PhaseParams, SSSFConfig
from .runner import Run
from .tracer import Tracer
from .utils import classify_slug_hint, engineer_name, new_id


def _finalize_when_killed(run: Run) -> None:
    """A killed run still closes its own trace.

    Python's default SIGTERM handling exits without unwinding, so `just kill`
    (or any `kill <pid>`) would leave the session reading `running` forever and
    its process rows open — the trace would claim work is in flight that is
    already dead. Turning the signal into SystemExit both finalizes here and
    lets the phase context manager record the phase as failed on the way out.
    """
    def handler(signum, _frame):
        run.tracer.session_finish(run.adw_id, ok=False)   # also closes process rows
        raise SystemExit(128 + signum)

    for sig in (signal.SIGTERM, signal.SIGINT):
        signal.signal(sig, handler)

def ensure(cfg: SSSFConfig, adw_id: str | None = None, *, mutates_repo: bool = False,
           slug_hint: str | None = None) -> Run:
    adw_id = adw_id or new_id(8)
    if slug_hint is None:
        slug_hint = os.environ.get("FACTORY_RUN_SLUG_HINT")
    slug_hint_is_path = False
    classified = classify_slug_hint(slug_hint)
    if classified is not None:
        slug_hint, slug_hint_is_path = classified
    else:
        slug_hint = None
    tracer = Tracer(cfg.observability.db,
                    f"{cfg.defaults.data_dir}/sessions/{adw_id}/events.jsonl")
    run = Run(cfg=cfg, adw_id=adw_id, tracer=tracer, engineer=engineer_name())
    adw_script_stem = Path(sys.argv[0]).stem
    run.adw_name = adw_script_stem
    request_id = os.environ.get("FACTORY_REQUEST_ID")
    if request_id is not None and (
        not request_id or request_id != request_id.strip() or len(request_id) > 240
        or any(ord(character) < 32 or ord(character) == 127 for character in request_id)
    ):
        raise ValueError("FACTORY_REQUEST_ID must be 1-240 visible characters without surrounding whitespace")
    tracer.session_start(adw_id, run.engineer, adw_name=adw_script_stem,
                         repo=run.repo_root, preset=cfg.preset, slug_hint=slug_hint,
                         slug_hint_is_path=slug_hint_is_path,
                         adw_script_stem=adw_script_stem, request_id=request_id)
    supervisor.started(adw_id)
    notify.notify_start(adw_id, adw_script_stem, slug_hint)
    # This process is the run. Record it before any phase opens, so a run that
    # hangs in its first agent call is still killable by adw_id.
    parent_command = shlex.join(
        [sys.executable, str(Path(sys.argv[0]).resolve()), *sys.argv[1:]],
    )
    tracer.process_start(
        adw_id, "adw", "", os.getpid(), parent_command,
        start_ticks=read_proc_start_ticks(os.getpid()),
    )
    _finalize_when_killed(run)
    run.console.session_started(adw_id, run.engineer)
    if mutates_repo:
        with run.phase(PhaseParams(
                name="git_preflight", task_id="git_preflight", kind="code", owner="git",
                description="Prove the target tree is clean and isolated before agents may edit it")) as ph:
            known_adw_ids = {
                row[0] for row in tracer.conn.execute(
                    "SELECT adw_id FROM sessions WHERE repo=?", (str(run.repo_root),)
                )
            }
            branch = git_helper.preflight(
                adw_id, cfg.defaults.git_branch_mode,
                spec_output_dir=permissions.plan_output_directory(cfg),
                known_adw_ids=known_adw_ids,
            )
            ph.log(branch=branch, mode=cfg.defaults.git_branch_mode)
    return run
