"""워커 세션 기록.

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

import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Callable

from .domain.worker_presentation import strip_terminal_colors

OKSTRA = "okstra"
_RESET = "\x1b[0m"
_MUTED = "\x1b[90m"
_LIVE_COLORS = (
    ("→ ", "\x1b[36m"),
    ("← ok", "\x1b[32m"),
    ("← error", "\x1b[31m"),
    ("!! PERMISSION DENIED", "\x1b[1;31m"),
    ("← done", "\x1b[33m"),
)

# 파일 사본이 담는 워커 진행 줄의 상한. 진행은 워커 출력의 부피가 몰리는
# 자리다 — 파일 하나만 읽는 디스패치도 킬로바이트 단위 도구 에코를 남기고,
# 관측된 사이드카는 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
        # cmux 워커는 색상 사용을 명시한다. 리드에서 상속한 비대화형 출력
        # 설정이 워커 터미널의 색상까지 끄지 않도록 명시적 요청을 우선한다.
        force_color = os.environ.get("FORCE_COLOR", "")
        self._color = (
            live
            and sys.stdout.isatty()
            and (
                force_color not in ("", "0")
                or (
                    force_color != "0"
                    and not os.environ.get("NO_COLOR")
                    and os.environ.get("TERM") != "dumb"
                )
            )
        )
        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, line)
        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), line)
        for line in archive:
            self._keep(self._row(speaker, line), capped=True)

    def _row(self, speaker: str, line: str) -> str:
        label = f"[{speaker}]"
        return f"{self._clock()} {label}{line}".rstrip()

    def _show(self, row: str, line: str) -> None:
        # 화면은 상한과 무관하다. 사람이 보고 있는 것을 잘라 낼 이유가 없다.
        if self._live:
            if self._color:
                color = next(
                    (color for prefix, color in _LIVE_COLORS if line.startswith(prefix)),
                    "",
                )
                if line.lstrip().startswith("… +"):
                    color = "\x1b[33m"
                # 시각·화자는 낮은 대비로 두고 본문은 터미널 기본색을 유지한다.
                prefix_size = len(row) - len(line.rstrip())
                row = (
                    f"{_MUTED}{row[:prefix_size]}{_RESET}"
                    f"{color}{row[prefix_size:]}{_RESET}"
                )
            print(row if self._color else strip_terminal_colors(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(strip_terminal_colors(row) + "\n")
        self._file.flush()

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