"""The live/ channel — append-only NDJSON session logs + the durable run-record shadow.

A traced op streams fed-session events to ``live/sessions/<id>.ndjson`` (ephemeral,
gitignored, never gated), updates ``live/CURRENT``, and on completion writes a gated
run-record into ``generated/runs/`` and refreshes ``generated/`` (so the record is
folded into the manifest). Live = "what's happening" (telemetry); run-record =
"what happened" (provenance). Never conflated.
"""

from __future__ import annotations

import hashlib
import json
import subprocess
import time
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any

from . import canonical
from .generate import generate

if TYPE_CHECKING:
    from .provider import DiskProvider


def _now_iso() -> str:
    return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")


def _session_id(op: str, started_at: str) -> str:
    stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S")
    digest = hashlib.sha1(f"{op}{started_at}".encode()).hexdigest()[:7]  # noqa: S324 (non-crypto id)
    return f"sess_{stamp}_{digest}"


class SessionWriter:
    """Appends NDJSON fed-session events with a monotonic seq; owns live/CURRENT."""

    def __init__(self, provider: DiskProvider, session_id: str) -> None:
        self.provider = provider
        self.session_id = session_id
        self.seq = 0
        self.path = provider.live_dir / "sessions" / f"{session_id}.ndjson"
        self.path.parent.mkdir(parents=True, exist_ok=True)
        (provider.live_dir / "CURRENT").write_text(session_id + "\n", encoding="utf-8")

    def emit(self, kind: str, **fields: object) -> None:
        self.seq += 1
        event = {
            "v": "0.1",
            "session": self.session_id,
            "seq": self.seq,
            "ts": _now_iso(),
            "kind": kind,
            **{k: v for k, v in fields.items() if v is not None},
        }
        with self.path.open("a", encoding="utf-8") as handle:
            handle.write(json.dumps(event, ensure_ascii=False) + "\n")

    def clear_current(self) -> None:
        current = self.provider.live_dir / "CURRENT"
        if current.is_file():
            current.unlink()


def current_session(provider: DiskProvider) -> str | None:
    current = provider.live_dir / "CURRENT"
    if not current.is_file():
        return None
    return current.read_text(encoding="utf-8").strip() or None


def read_session(provider: DiskProvider, session_id: str) -> list[dict[str, Any]]:
    path = provider.live_dir / "sessions" / f"{session_id}.ndjson"
    if not path.is_file():
        return []
    return [
        json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()
    ]


@dataclass(frozen=True)
class RunOutcome:
    session: str
    result: str
    duration_s: int
    run_record: str
    live_log: str


def run_traced(
    provider: DiskProvider, *, op: str, command: list[str], task: str | None = None
) -> RunOutcome:
    """Run a real op, stream live events, write a gated run-record, refresh generated/."""
    started_at = _now_iso()
    session = _session_id(op, started_at)
    writer = SessionWriter(provider, session)
    writer.emit("session_start", op=op, task=task)
    start = time.monotonic()
    try:
        with subprocess.Popen(  # noqa: S603 (command is operator-supplied, by design)
            command,
            cwd=str(provider.project_root),
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,
        ) as proc:
            assert proc.stdout is not None
            for line in proc.stdout:
                writer.emit("log", level="info", msg=line.rstrip("\n"))
            returncode = proc.wait()
        result = "ok" if returncode == 0 else "fail"
    except OSError as exc:
        writer.emit("error", msg=f"could not launch: {exc}")
        result = "fail"
    duration_s = int(time.monotonic() - start)

    relpath = _write_run_record(
        provider,
        session=session,
        op=op,
        task=task,
        started_at=started_at,
        duration_s=duration_s,
        result=result,
    )
    # refresh generated/ so the new run-record is folded into the manifest
    generate(provider, generated_at=_now_iso(), write=True)
    writer.emit("session_end", result=result, duration_s=duration_s, run_record=relpath)
    writer.clear_current()
    return RunOutcome(
        session=session,
        result=result,
        duration_s=duration_s,
        run_record=relpath,
        live_log=str(writer.path),
    )


def _write_run_record(
    provider: DiskProvider,
    *,
    session: str,
    op: str,
    task: str | None,
    started_at: str,
    duration_s: int,
    result: str,
) -> str:
    record: dict[str, Any] = {
        "schema_version": "0.1",
        "kind": "run_record",
        "session": session,
        "op": op,
        "started_at": started_at,
        "duration_s": duration_s,
        "result": result,
    }
    if task:
        record["task"] = task
    relpath = f"runs/{session}.json"
    target = provider.generated_dir / relpath
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_bytes(canonical.dumpb(record))
    return relpath
