"""워커 세션 기록.

한 줄에 시각·화자·본문 셋을 담는다. 나중에 리드가 같은 파일에 쓰면 화자만
달라지므로, 지금 형식을 정해 두면 그때 파일이 바뀌지 않는다.
"""
from __future__ import annotations

from datetime import datetime
from pathlib import Path
from typing import Callable

OKSTRA = "okstra"
_SPEAKER_WIDTH = 14

# 파일 사본이 담는 워커 진행 줄의 상한. 진행은 워커 출력의 부피가 몰리는
# 자리다 — 파일 하나만 읽는 디스패치도 킬로바이트 단위 도구 에코를 남기고,
# 관측된 사이드카는 8MB 에 이르러 프로젝트의 `.okstra/` 바이트를 지배했다.
# 상한은 블록이 아니라 run 전체에 건다: 블록 경계는 공급자 자신의 어휘이고
# 이 기록기는 그것을 모른다. 그 대가로 아주 긴 run 은 표본이 아니라 앞부분을
# 남기며, 생략 표시가 그 사실을 드러낸다.
LOG_LINE_CAP = 5000
_ELISION_NOTICE_EVERY = 500


def _now() -> str:
    return datetime.now().strftime("%H:%M:%S")


class SessionTranscript:
    """워커 한 명의 세션 기록. 화면 출력도 함께 맡는다."""

    def __init__(
        self,
        path: Path,
        *,
        live: bool,
        clock: Callable[[], str] = _now,
    ) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        self._file = path.open("w", encoding="utf-8")
        self._live = live
        self._clock = clock
        self._archived = 0
        self._elided = 0

    def write(self, speaker: str, line: str, *, capped: bool = True) -> None:
        """한 줄을 화면과 기록에 남긴다.

        ``capped=False`` 는 상한을 넘겨도 반드시 파일에 남길 줄이다 — 워커의
        결론과 okstra 자신의 기록이 그렇다. 잘린 도구 에코는 세부를 잃지만
        잘린 결론은 사후 분석 전체를 잃는다.
        """
        row = self._row(speaker, line)
        self._show(row)
        self._keep(row, capped=capped)

    def write_event(
        self, speaker: str, *, screen: list[str], archive: list[str]
    ) -> None:
        """한 이벤트의 두 투영을 각자의 목적지로 보낸다.

        화면은 요약을 받고 기록은 본문까지 받는다. 둘 중 하나를 골라 양쪽에
        쓰던 동안에는 pane 이 붙은 run 의 기록이 화면 투영으로 대체됐다 —
        도구 결과가 `← done (23 bytes)` 한 줄로만 남고 그 23 바이트가 무엇이
        었는지는 파일 어디에도 없었다. antigravity `view_file` 은 파일 내용이
        아니라 `721 lines, 114306 bytes` 같은 요약을 돌려주므로, 본문이 빠진
        기록에서는 46KB 를 읽은 호출과 잘린 호출이 같은 줄로 보인다.
        """
        for line in screen:
            self._show(self._row(speaker, line))
        for line in archive:
            self._keep(self._row(speaker, line), capped=True)

    def _row(self, speaker: str, line: str) -> str:
        # 패딩이 본문 앞 공백을 겸한다. 뒤에 공백을 한 칸 더 넣으면
        # `[worker:grok]`(13칸) 줄이 두 칸이 되고 `[okstra]` 정렬이 깨진다.
        label = f"[{speaker}]".ljust(_SPEAKER_WIDTH)
        return f"{self._clock()} {label}{line}".rstrip()

    def _show(self, row: str) -> None:
        # 화면은 상한과 무관하다. 사람이 보고 있는 것을 잘라 낼 이유가 없다.
        if self._live:
            print(row, flush=True)

    def _keep(self, row: str, *, capped: bool) -> None:
        if not capped:
            self._append(row)
            return
        if self._archived < LOG_LINE_CAP:
            self._archived += 1
            self._append(row)
            return
        self._elided += 1
        # 끝에서 한 번이 아니라 주기적으로 남긴다. 로그를 tail 하는 사람이
        # run 이 아직 진행 중임을 봐야 하고, 멈춘 파일과 구별되어야 한다.
        if self._elided % _ELISION_NOTICE_EVERY == 0:
            self._note_elision()

    def note(self, line: str) -> None:
        """okstra 자신이 남기는 줄. 워커의 말과 섞이되 화자로 구분된다."""
        self.write(OKSTRA, line, capped=False)

    def close(self) -> None:
        if self._elided % _ELISION_NOTICE_EVERY:
            self._note_elision()
        self._file.close()

    def _append(self, row: str) -> None:
        self._file.write(row + "\n")
        self._file.flush()

    def _note_elision(self) -> None:
        self._append(
            f"  [okstra log-cap] {self._elided} progress line(s) elided"
        )
