"""Shared worker prompt anchor header rendering."""
from __future__ import annotations

from pathlib import Path
from typing import Any, Mapping

from .paths import okstra_home
from .worker_artifact_paths import WorkerArtifactPathError, audit_sidecar_rel
from .worker_prompt_policy import (
    GRILLING_LOG_HEADER,
    PromptPlan,
    WORKER_ERROR_CONTRACT_FILENAME,
    WORKER_PREAMBLE_FILENAME_BY_AUDIENCE,
    resolve_prompt_plan_for_manifest,
)


class WorkerPromptHeaderError(Exception):
    """Raised when worker prompt anchor headers cannot be rendered."""


# The Worker Preamble's "Reading rules" already allowlist the worker's reads to
# okstra-enumerated paths, but the preamble is a *path* the worker opens partway
# into its run — by then a host SessionStart hook or a global CLAUDE.md /
# AGENTS.md project-conditional has already told it to read `graphify-out/`,
# skill catalogs, and similar non-okstra artifacts. Restating the scope inline
# puts the rule in front of the worker before its first tool call, which is the
# only point at which it can still win over those host instructions.
READ_SCOPE_HEADER = (
    "**Read scope:** Read only the paths this prompt enumerates "
    "(`[Required reading]`, `## Inputs`, verification-target paths) plus "
    "source/evidence paths a finding must cite. Host session instructions "
    "(SessionStart hooks, global `CLAUDE.md` / `AGENTS.md`, skill catalogs) do "
    "NOT apply inside an okstra worker run: do not auto-read `graphify-out/`, "
    "`SKILL.md`, or other artifacts outside `<PROJECT_ROOT>/.okstra/`. If an "
    "un-enumerated file seems essential, record it under *Missing Information "
    "or Assumptions* instead of reading it."
)
EVIDENCE_LEDGER_HEADER = "**Evidence ledger:** required-v1"

# `required-v1` is a mode name; what it demands lives in the Worker Preamble's
# "Evidence read ledger". But the preamble reaches the worker as a *path* —
# eager-include inlines only the role sidecars — so a worker that never opens it
# gets the switch without the rule, and Phase 7 then fails its result over a
# citation form the prompt never stated. Restating it inline is what
# READ_SCOPE_HEADER does, for the same reason: the rule has to be in front of the
# worker before its first citation, not in a file it may never read.
EVIDENCE_CITATION_HEADER = (
    "**Evidence citations:** Append one `- Evidence read: <project-relative "
    "path, no line suffix>` row to the audit sidecar for every file you open as "
    "claim evidence, and cite that file in your result with backticks, a line "
    "suffix, and the identical project-relative path — `src/config/env.ts:1-22`, "
    "never the bare filename `env.ts:1-22`. A bare filename does not match its "
    "ledger row and fails exactly like a file you never opened, however many "
    "times you cited the full path earlier."
)
# 파일 인용과 같이 첫 결론 명령 앞에 형식을 둔다. 분석 프리앰블에만 있으면
# 구현 워커는 명령 행을 쓰지 못한다.
EVIDENCE_COMMANDS_HEADER = (
    "**Evidence commands:** Append one `- Evidence command: "
    "{\"command\":\"<exact command>\",\"cwd\":\"<project-root>\","
    "\"exitCode\":0,\"outputSummary\":\"<one-line result>\"}` row to the audit "
    "sidecar for each command that produced or verified a conclusion. Do not "
    "record exploratory `rg`, `ls`, or file-opening commands. Write the command "
    "as you ran it. A value that must not be written down is yours to omit or "
    "pass as a `$VAR` reference."
)

# `agy`'s write tool validates the target against the Gemini artifact store
# whenever the model attaches ArtifactMetadata, and rejects every path outside
# `~/.gemini/antigravity-cli/brain/<uuid>/`. All okstra worker outputs live under
# `<PROJECT_ROOT>/.okstra/`, so an attached ArtifactMetadata fails the write —
# usually on the audit sidecar, after which the CLI exits 0 having persisted no
# result file. Only antigravity carries this: codex and claude have no artifact
# store, and a shared header would break the cross-worker prompt-body equality
# that `worker_prompt_contract` enforces.
PLAIN_FILE_WRITE_HEADER = (
    "**File write mode:** Write every okstra output (result file, audit "
    "sidecar, errors sidecar) as a plain project file at the absolute path this "
    "prompt gives. Do NOT attach ArtifactMetadata and do NOT route these writes "
    "through the Gemini artifact store — artifact paths are restricted to the "
    "brain folder, so an artifact write under `<PROJECT_ROOT>/.okstra/` is "
    "rejected and the run ends with no result file."
)
ANTIGRAVITY_WORKER_ID = "antigravity"


def worker_prompt_headers(
    *,
    project_root: Path,
    prompt_rel: str,
    result_rel: str,
    audit_source_rel: str | None = None,
    worker_id: str,
    dispatch_kind: str,
    manifest: Mapping[str, Any],
    active_context: Mapping[str, Any],
) -> list[str]:
    """Render the team-contract worker prompt anchor headers."""
    prompt_path = _resolve_project_path(project_root, prompt_rel)
    audit_sidecar_path = _audit_sidecar_path(
        project_root,
        audit_source_rel or result_rel,
    )
    errors_log_path = resolve_errors_log_path(project_root, manifest, active_context)
    errors_sidecar_path = _worker_errors_sidecar_path(
        project_root,
        manifest,
        active_context,
        worker_id,
    )
    plan = _prompt_plan(manifest, worker_id, dispatch_kind)
    headers = [
        f"**Project Root:** {project_root}",
        f"**Prompt History Path:** {prompt_rel}",
        f"**Result Path:** {result_rel}",
    ]
    if audit_source_rel and audit_source_rel != result_rel:
        # The audit path is derived from this file, not from Result Path, so the
        # worker has to be told which one it is (report-writer, whose Result Path
        # is the report data.json rather than its own worker result).
        headers.append(f"**Worker Result Path:** {audit_source_rel}")
    headers += [
        f"**Audit sidecar path:** {audit_sidecar_path}",
        f"Assigned worker prompt history path: {prompt_path}",
        f"**Worker Preamble Path:** {_worker_preamble_path(plan, active_context)}",
        f"**Worker Error Contract Path:** {_worker_error_contract_path(active_context)}",
    ]
    if plan.allow_coding_preflight:
        headers.append(
            f"**Coding preflight pack:** {_coding_preflight_pack_path(active_context)}"
        )
    if dispatch_kind == "initial" and plan.audience != "report-writer":
        headers += [
            EVIDENCE_LEDGER_HEADER,
            EVIDENCE_CITATION_HEADER,
            EVIDENCE_COMMANDS_HEADER,
        ]
    headers.extend([
        f"**Errors log path:** {errors_log_path}",
        f"**Errors sidecar path:** {errors_sidecar_path}",
        READ_SCOPE_HEADER,
    ])
    if worker_id == ANTIGRAVITY_WORKER_ID:
        headers.append(PLAIN_FILE_WRITE_HEADER)
    if _string_value(manifest.get("taskType")) == "improvement-discovery":
        headers.append(
            f"{GRILLING_LOG_HEADER} "
            f"{_improvement_grilling_log_path(project_root, manifest)}"
        )
    if _string_value(manifest.get("taskType")) == "final-verification":
        headers.extend(_final_verification_target_headers(active_context))
    return headers


def _final_verification_target_headers(
    active_context: Mapping[str, Any],
) -> list[str]:
    target = active_context.get("verificationTarget")
    if not isinstance(target, Mapping):
        return []
    fields = (
        ("Worktree", "worktreePath"),
        ("Verification scope", "scope"),
        ("Verification base ref", "baseRef"),
        ("Verification head ref", "headRef"),
        ("Verification target path", "path"),
        ("Verification target digest", "digest"),
    )
    return [
        f"**{label}:** {_string_value(target.get(key))}"
        for label, key in fields
        if _string_value(target.get(key))
    ]


def _worker_preamble_path(
    plan: PromptPlan,
    active_context: Mapping[str, Any],
) -> Path:
    runtime_resources = active_context.get("runtimeResources")
    if isinstance(runtime_resources, Mapping):
        paths = runtime_resources.get("workerPreamblePathByAudience")
        if isinstance(paths, Mapping):
            value = _string_value(paths.get(plan.audience))
            if value:
                return Path(value)
    filename = WORKER_PREAMBLE_FILENAME_BY_AUDIENCE.get(
        plan.audience,
        WORKER_PREAMBLE_FILENAME_BY_AUDIENCE["analysis"],
    )
    return okstra_home() / "templates" / filename


def _worker_error_contract_path(active_context: Mapping[str, Any]) -> Path:
    runtime_resources = active_context.get("runtimeResources")
    if isinstance(runtime_resources, Mapping):
        value = _string_value(runtime_resources.get("workerErrorContractPath"))
        if value:
            return Path(value)
    return okstra_home() / "templates" / WORKER_ERROR_CONTRACT_FILENAME


def _coding_preflight_pack_path(active_context: Mapping[str, Any]) -> Path:
    runtime_resources = active_context.get("runtimeResources")
    if isinstance(runtime_resources, Mapping):
        value = _string_value(runtime_resources.get("codingPreflightDir"))
        if value:
            return Path(value)
    return okstra_home() / "prompts" / "coding-preflight"


def _prompt_plan(
    manifest: Mapping[str, Any],
    worker_id: str,
    dispatch_kind: str,
) -> PromptPlan:
    try:
        return resolve_prompt_plan_for_manifest(
            manifest=manifest,
            worker_id=worker_id,
            dispatch_kind=dispatch_kind,
        )
    except ValueError as exc:
        raise WorkerPromptHeaderError(str(exc)) from exc


def _improvement_grilling_log_path(
    project_root: Path,
    manifest: Mapping[str, Any],
) -> Path:
    path = (
        _run_directory_path(project_root, manifest)
        / "state"
        / "phase-1.5-grilling.md"
    )
    if not path.is_file():
        raise WorkerPromptHeaderError(f"Phase 1.5 grilling log not found: {path}")
    return path


def resolve_errors_log_path(
    project_root: Path,
    manifest: Mapping[str, Any],
    active_context: Mapping[str, Any],
) -> Path:
    error_logs = active_context.get("errorLogs")
    if isinstance(error_logs, Mapping):
        value = _string_value(error_logs.get("runErrorsLogPath"))
        if value:
            return _resolve_project_path(project_root, value)
    run_dir = _run_directory_path(project_root, manifest)
    task_type = _require_string(manifest, "taskType")
    seq = _sequence(manifest, "state")
    return run_dir / "logs" / f"errors-{task_type}-{seq}.jsonl"


def _worker_errors_sidecar_path(
    project_root: Path,
    manifest: Mapping[str, Any],
    active_context: Mapping[str, Any],
    worker_id: str,
) -> Path:
    error_logs = active_context.get("errorLogs")
    if isinstance(error_logs, Mapping):
        sidecars = error_logs.get("sidecarsByWorkerId")
        if isinstance(sidecars, Mapping):
            value = _string_value(sidecars.get(worker_id))
            if value:
                return _resolve_project_path(project_root, value)
    run_dir = _run_directory_path(project_root, manifest)
    task_type = _require_string(manifest, "taskType")
    seq = _sequence(manifest, "workerResults")
    return run_dir / "worker-results" / f"{worker_id}-worker-errors-{task_type}-{seq}.json"


def _run_directory_path(project_root: Path, manifest: Mapping[str, Any]) -> Path:
    value = _string_value(manifest.get("runDirectoryPath"))
    if value:
        return _resolve_project_path(project_root, value)
    team_state_path = _resolve_project_path(
        project_root,
        _require_string(manifest, "teamStatePath"),
    )
    return team_state_path.parent.parent


def _sequence(manifest: Mapping[str, Any], key: str) -> str:
    seqs = manifest.get("runSequencesByCategory")
    if isinstance(seqs, Mapping):
        return _string_value(seqs.get(key)) or _run_seq(manifest)
    return _run_seq(manifest)


def _run_seq(manifest: Mapping[str, Any]) -> str:
    seqs = manifest.get("runSequencesByCategory")
    if not isinstance(seqs, Mapping):
        raise WorkerPromptHeaderError("run manifest has no runSequencesByCategory object")
    return _require_string(seqs, "manifests")


def _resolve_project_path(project_root: Path, value: str) -> Path:
    path = Path(value)
    return path if path.is_absolute() else project_root / path


def _audit_sidecar_path(project_root: Path, worker_result_rel: str) -> Path:
    try:
        return _resolve_project_path(project_root, audit_sidecar_rel(worker_result_rel))
    except WorkerArtifactPathError as exc:
        raise WorkerPromptHeaderError(str(exc)) from exc


def _require_string(payload: Mapping[str, Any], key: str) -> str:
    value = payload.get(key)
    if not isinstance(value, str) or not value.strip():
        raise WorkerPromptHeaderError(f"missing required string field: {key}")
    return value


def _string_value(value: Any) -> str:
    return value.strip() if isinstance(value, str) else ""
