"""Provider-neutral execution contracts shared by the dispatcher and workers.

A worker run is decided along three axes: which terminal surface it lands on,
which provider CLI runs it, and which role it plays. This module owns the
provider axis' vocabulary plus the one rule that is identical for every worker
regardless of provider — see ``ExecutionPolicy``.
"""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Protocol, runtime_checkable

from .worker_presentation import Presentation
from ..write_policy import WriteEnforcement, WritePolicy

SERVED_MODEL_MISMATCH_EXIT_CODE = 78


@dataclass(frozen=True)
class ExecutionPolicy:
    """How a non-interactive worker is allowed to act.

    Identical for every worker: nobody is at the keyboard to answer an approval
    prompt, so the gate has to be open and the boundary has to come from a
    sandbox instead. Declared once here rather than per provider, because a
    policy that lives in one adapter file per provider drifts into one policy
    per provider.
    """

    auto_approve: bool
    write_scope: tuple[Path, ...]
    write_policy: WritePolicy | None = None
    write_enforcement: WriteEnforcement | None = None


@dataclass(frozen=True)
class WorkerExecRequest:
    """One worker dispatch, before any provider has looked at it.

    ``worktree_path`` is the stage tree this run was given, or None for a
    dispatch that works in the project itself. It is kept apart from
    ``policy.write_scope`` because the two answer different questions: the scope
    is what may be written, while this is where the run belongs. Providers
    disagree on that second answer, so the request carries both and each
    strategy decides.
    """

    prompt_text: str
    model: str
    project_root: Path
    worktree_path: Path | None
    policy: ExecutionPolicy
    idle_timeout_seconds: int
    # The id the dispatcher chose, empty when it chose none. Only a CLI that
    # accepts a session id reads it. Handing it over means the CLI does not
    # issue its own, so the recorded id and the session's own jsonl are
    # guaranteed to name the same session.
    session_id: str = ""


@dataclass(frozen=True)
class ExecCommand:
    """공급자 호출 하나, 완전히 해소된 상태.

    ``stdin_text`` 는 프롬프트를 인자로 받는 CLI 에서 None 이다.

    ``cwd`` 가 계약에 있는 이유는 argv 에서 유도할 수 없기 때문이다 — 어떤
    CLI 는 작업 디렉터리를 플래그로 받고 어떤 CLI 는 프로세스의 것을 물려받는데,
    두 무리가 같은 디렉터리를 고르지 않는다.

    ``presentation`` 은 이 CLI 의 출력을 무엇으로 볼 것인가를 정한다. 스트림
    형식을 따로 선언하고 읽는 법을 나중에 붙이던 구조에서는, 형식만 선언하고
    읽는 법이 어긋난 공급자의 화면이 런 내내 비어 있었다. 둘을 한 값으로 묶어
    그 상태를 표현 불가능하게 만든다.
    """

    argv: tuple[str, ...]
    stdin_text: str | None
    cwd: Path
    presentation: Presentation


@dataclass(frozen=True)
class WorkerWriteCapability:
    """Maximum write-boundary precision exposed by a worker runner."""

    max_boundary_precision: Literal[
        "exact-path",
        "directory-boundary",
        "none",
    ]


@dataclass(frozen=True)
class PolicySupport:
    """Whether a provider CLI can express ``ExecutionPolicy`` at all.

    A provider that cannot must say so rather than quietly accepting the policy
    and running without it. ``note`` carries the reason and is what the contract
    test asserts on.
    """

    can_auto_approve: bool
    can_bound_write_scope: bool
    note: str = ""

    def worker_write_capability(self) -> WorkerWriteCapability:
        precision = (
            "directory-boundary" if self.can_bound_write_scope else "none"
        )
        return WorkerWriteCapability(precision)


@runtime_checkable
class ExecutionStrategy(Protocol):
    def build_command(self, request: WorkerExecRequest) -> ExecCommand: ...

    def policy_support(self) -> PolicySupport: ...
