"""Reader for okstra worker wrapper status sidecars."""
from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping

from .json_boundary import JsonBoundaryError, load_owned_object


@dataclass(frozen=True)
class WrapperStatus:
    path: Path
    stage: str
    exit_code: int | None
    timeout: bool
    log_path: Path | None
    raw: Mapping[str, Any]

    @property
    def is_terminal(self) -> bool:
        return self.stage == "exited"


def status_path_for_prompt(prompt_path: Path) -> Path:
    return prompt_path.with_suffix(prompt_path.suffix + ".status.json")


def log_path_for_prompt(prompt_path: Path) -> Path:
    """Where the wrapper writes its live log for this prompt.

    A `.md` prompt drops that suffix rather than stacking on it, so the log of
    `…-009.md` is `…-009.log` and not `…-009.md.log`. Both spellings existed:
    the entrypoint wrote the first while the dispatcher listed the second among
    the write policy's allowed artifacts, so every CLI worker's own log read as
    an unauthorized change to the artifact root. One function now, because the
    disagreement stays invisible until an audit compares the two.
    """
    if prompt_path.name.endswith(".md"):
        return prompt_path.with_name(f"{prompt_path.name[:-3]}.log")
    return Path(f"{prompt_path}.log")


def mutation_snapshot_path_for_prompt(prompt_path: Path) -> Path:
    """Where okstra writes this prompt's pre-dispatch mutation snapshot."""
    return prompt_path.with_suffix(prompt_path.suffix + ".mutation-audit.json")


def prompt_derived_paths(prompt_path: Path) -> tuple[Path, ...]:
    """The three files okstra writes beside a prompt: status, log, snapshot.

    They are written together and must be authorised together. Listing them by
    hand let the two sides drift: dispatch added the snapshot to its write
    policy while `agent-prompt materialize` kept its own five-path list, so the
    two policies hashed differently and `record_invocation_attempt` refused
    every dynamically materialized invocation as `invocationRef drift` — which
    took Phase 5.5 reverify with it. One function, so a fourth derived file
    cannot reach one caller and miss the other.
    """
    return (
        status_path_for_prompt(prompt_path),
        log_path_for_prompt(prompt_path),
        mutation_snapshot_path_for_prompt(prompt_path),
    )


def read_wrapper_status(path: Path) -> WrapperStatus | None:
    try:
        raw = load_owned_object(path, artifact="worker wrapper status")
    except (OSError, JsonBoundaryError):
        return None
    if not isinstance(raw, dict):
        return None
    stage = raw.get("stage")
    if not isinstance(stage, str) or not stage:
        return None
    return WrapperStatus(
        path=path,
        stage=stage,
        exit_code=_optional_int(raw.get("exit_code")),
        timeout=raw.get("timeout") is True,
        log_path=_optional_path(raw.get("log_path")),
        raw=raw,
    )


def _optional_int(value: object) -> int | None:
    return value if isinstance(value, int) and not isinstance(value, bool) else None


def _optional_path(value: object) -> Path | None:
    if not isinstance(value, str) or not value.strip():
        return None
    return Path(value)
