"""
job_log.py - Capture pipeline stdout/stderr into a per-job log.

The pipeline already prints stage progress via print(). For a reviewable
post-run log we tee those lines into memory + outputs/<job_id>/job.log
without rewriting every call site.
"""

import sys
import time
from contextlib import contextmanager
from typing import Callable, Iterator, List, Optional, TextIO


class _LineSplitter:
    """Accumulate write() chunks and emit complete lines."""

    def __init__(self, on_line: Callable[[str], None]):
        self._buf = ""
        self._on_line = on_line

    def write(self, s: str) -> None:
        if not s:
            return
        self._buf += s
        while "\n" in self._buf:
            line, self._buf = self._buf.split("\n", 1)
            # Strip trailing CR from Windows-ish streams; keep content intact.
            self._on_line(line.rstrip("\r"))

    def flush_remainder(self) -> None:
        if self._buf:
            self._on_line(self._buf.rstrip("\r"))
            self._buf = ""


class _Tee:
    """Mirror writes to the original stream and a line callback."""

    def __init__(self, stream: TextIO, on_line: Callable[[str], None]):
        self._stream = stream
        self._lines = _LineSplitter(on_line)

    def write(self, s: str) -> int:
        n = self._stream.write(s)
        self._stream.flush()
        self._lines.write(s)
        return n

    def flush(self) -> None:
        self._stream.flush()

    def flush_remainder(self) -> None:
        self._lines.flush_remainder()

    def __getattr__(self, name: str):
        return getattr(self._stream, name)


def stamp_line(line: str, t: Optional[float] = None) -> str:
    """Prefix a log line with HH:MM:SS."""
    ts = time.strftime("%H:%M:%S", time.localtime(t if t is not None else time.time()))
    return f"[{ts}] {line}"


@contextmanager
def capture_stdio(on_line: Callable[[str], None]) -> Iterator[None]:
    """
    Tee sys.stdout and sys.stderr into on_line(raw_line) for the duration.

    Safe for the single-job-at-a-time usage of this app; overlapping jobs
    would interleave (same limitation as the LLM cost counters).
    """
    old_out, old_err = sys.stdout, sys.stderr
    tee_out = _Tee(old_out, on_line)
    tee_err = _Tee(old_err, on_line)
    sys.stdout = tee_out  # type: ignore[assignment]
    sys.stderr = tee_err  # type: ignore[assignment]
    try:
        yield
    finally:
        tee_out.flush_remainder()
        tee_err.flush_remainder()
        sys.stdout = old_out
        sys.stderr = old_err


def read_log_file(path: str) -> List[str]:
    try:
        with open(path, encoding="utf-8") as f:
            return [ln.rstrip("\n") for ln in f]
    except OSError:
        return []
