"""워커 출력의 표현 전략.

공급자 CLI 는 저마다 다른 것을 뱉는다 — 사람이 읽는 텍스트를 흘리는 CLI 도
있고, 자기 어휘의 JSON 을 흘리는 CLI 도 있다. okstra 가 그 형식을 알아야만
화면이 나오는 구조에서는 공급자가 형식을 바꿀 때마다 화면이 조용히 빈다.
그래서 "해석하지 않는다" 를 1급 선택지로 둔다.

전략은 두 가지를 답한다. stderr 를 stdout 에 합칠 것인가, 그리고 각 스트림의
줄을 누가 받는가. 둘을 함께 두는 이유는 배치와 해석이 짝이기 때문이다 —
JSON 을 요청하면 한 스트림으로 합쳐 읽어야 하고, 결과와 진행을 나눠 내는
CLI 는 갈라 읽어야 한다.
"""
from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Literal, Mapping, Protocol, runtime_checkable

from .worker_stream import (
    Normalise,
    StreamEvent,
    Text,
    final_text,
    format_live,
    format_log,
)

Sink = Callable[[str], str | None]
Channel = Literal["stdout", "stderr"]
SinkSpec = tuple[Channel, Sink]
ObserveServedModel = Callable[[Mapping[str, Any]], str | None]

WORKER = "worker"


class TranscriptWriter(Protocol):
    """세션 기록에 남긴다. 화면 출력도 이 구현이 함께 맡는다."""

    def write(self, speaker: str, line: str) -> None: ...

    def write_event(
        self, speaker: str, *, screen: list[str], archive: list[str]
    ) -> None: ...


@runtime_checkable
class Presentation(Protocol):
    def merges_stderr(self) -> bool: ...

    def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]: ...

    def flush(self, writer: TranscriptWriter) -> None: ...


@dataclass(frozen=True)
class MergedText:
    """CLI 가 사람에게 보여주는 출력을 그대로 흘린다.

    한 스트림에 진행과 결과가 함께 온다. 해석하지 않으므로 공급자가 형식을
    바꿔도 화면이 깨지지 않는다.
    """

    served_model_at_exit: Callable[[str, Path], str | None] | None = None

    def merges_stderr(self) -> bool:
        return True

    def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
        def emit(line: str) -> str | None:
            writer.write(WORKER, line)
            return line

        return (("stdout", emit),)

    def flush(self, writer: TranscriptWriter) -> None:
        return


@dataclass(frozen=True)
class SplitText:
    """결과와 진행을 다른 스트림으로 내는 CLI.

    stdout 은 답이고 stderr 는 진행이다. 답은 어느 모드에서도 호출자에게
    가야 하므로 종결 텍스트로 돌려주고, 진행은 기록과 화면에만 남는다.
    """

    def merges_stderr(self) -> bool:
        return False

    def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
        def result(line: str) -> str | None:
            writer.write(WORKER, line)
            return line

        def progress(line: str) -> str | None:
            writer.write(WORKER, line)
            return None

        return (("stdout", result), ("stderr", progress))

    def flush(self, writer: TranscriptWriter) -> None:
        return


@dataclass(frozen=True)
class JsonEvents:
    """공급자 어휘의 JSON 을 공통 이벤트로 옮겨 적는다.

    사람이 읽는 출력에 진행이 없는 CLI 를 위한 경로다. 어휘를 아는 대가로
    도구 호출을 구조로 보여줄 수 있다.
    """

    normalise: Normalise
    observe: ObserveServedModel
    # 어떤 CLI 의 streaming-json 은 문장이 아니라 토큰을 한 줄씩 보낸다. 붙이지
    # 않으면 pane 이 한 글자 한 줄이 된다. 기본은 끄고, 그런 CLI 의 어댑터가 켠다 —
    # 어느 CLI 가 그런지는 이 계층이 알 필요도, 알아서도 안 되는 사실이다.
    join_adjacent_text: bool = False
    _pending_text: list[str] = field(default_factory=list, repr=False, compare=False)

    def merges_stderr(self) -> bool:
        return True

    def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
        def emit(line: str) -> str | None:
            stripped = line.strip()
            if not stripped:
                return None
            try:
                event = json.loads(stripped)
            except ValueError:
                # 이벤트가 아니다. CLI 자신의 오류 텍스트가 이 스트림으로
                # 오므로 삼키면 아무도 못 본다.
                self._publish_text(writer)
                writer.write(WORKER, stripped)
                return None
            if not isinstance(event, dict):
                return None
            self.observe(event)
            closing: str | None = None
            for entry in self.normalise(event):
                closing = self._write_entry(writer, entry) or closing
            return closing

        return (("stdout", emit),)

    def flush(self, writer: TranscriptWriter) -> None:
        self._publish_text(writer)

    def _write_entry(
        self, writer: TranscriptWriter, entry: StreamEvent
    ) -> str | None:
        if isinstance(entry, Text) and self.join_adjacent_text:
            self._pending_text.append(entry.body)
            self._publish_complete_lines(writer)
            return None
        self._publish_text(writer)
        # 두 투영을 고르는 것이 아니라 둘 다 보낸다. 화면은 요약, 기록은
        # 본문까지. 고르던 동안에는 pane 이 붙은 run 의 기록에서 도구 결과
        # 본문이 통째로 사라졌다.
        writer.write_event(
            WORKER,
            screen=format_live(entry),
            archive=format_log(entry),
        )
        return final_text(entry)

    def _publish_complete_lines(self, writer: TranscriptWriter) -> None:
        combined = "".join(self._pending_text)
        if "\n" not in combined:
            return
        *complete, remainder = combined.split("\n")
        self._pending_text.clear()
        if remainder:
            self._pending_text.append(remainder)
        self._write_text_rows(writer, "\n".join(complete))

    def _publish_text(self, writer: TranscriptWriter) -> None:
        combined = "".join(self._pending_text)
        self._pending_text.clear()
        self._write_text_rows(writer, combined)

    def _write_text_rows(self, writer: TranscriptWriter, body: str) -> None:
        event = Text(body=body)
        screen = format_live(event)
        archive = format_log(event)
        if screen or archive:
            writer.write_event(WORKER, screen=screen, archive=archive)
