"""Resolve worker prompt requirements from functional audience."""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Literal, Mapping

from .agent.invocation import AgentAudience
from .analysis_inputs import ANALYSIS_TASK_TYPES


PromptAudience = Literal[
    "analysis",
    "implementation-executor",
    "implementation-verifier",
    "report-writer",
    "translator",
    "reverify",
    "lead-only",
]

# The prompt emitter, the run validator, and the worker-facing docs all bind to
# this literal; restating it anywhere else silently forks the dispatch contract.
GRILLING_LOG_HEADER = "**Phase 1.5 Grilling Log:**"
# Workers extract both paths verbatim and abort with
# `<SENTINEL_PREFIX>_ERRORS_PATH_MISSING` when either is absent, because
# synthesizing them from a run-directory pattern is forbidden. A prompt that
# reaches a worker without them yields an empty run-level errors log.
ERRORS_PATH_HEADERS = (
    "**Errors log path:**",
    "**Errors sidecar path:**",
)
# The executor sidecar tells the worker to execute exactly one Stage Map stage
# against the approved plan, and forbids recomputing that stage from
# `consumers.jsonl`. Both facts therefore have to reach the worker prompt
# itself: the lead's launch prompt carries them, and no worker reads that.
APPROVED_PLAN_HEADER = "**Approved plan:**"
IMPLEMENTATION_STAGE_HEADER = "**Stage for this implementation run:**"
IMPLEMENTATION_HEADERS = (
    "**Worktree:**",
    APPROVED_PLAN_HEADER,
    IMPLEMENTATION_STAGE_HEADER,
)
FINAL_VERIFICATION_HEADERS = (
    "**Worktree:**",
    "**Verification scope:**",
    "**Verification base ref:**",
    "**Verification head ref:**",
    "**Verification target path:**",
    "**Verification target digest:**",
)
SUPPORTED_TASK_TYPES = frozenset({
    "technical-verification",
    "requirements-discovery",
    "error-analysis",
    "implementation-option-selection",
    "implementation-planning",
    "improvement-discovery",
    "implementation",
    "final-verification",
    "release-handoff",
    *ANALYSIS_TASK_TYPES,
})
# One duty per analysis ROLE, not per phase. Phases that ask their worker for the
# same kind of judgement share a contract — requirements- and improvement-discovery
# both hand over candidates they do not start — and a phase whose worker decides
# something else gets its own. A task type absent from this map takes the
# observational default below: describe the area, do not design for it.
ANALYSIS_DUTY_BY_TASK_TYPE: dict[str, AgentAudience] = {
    "technical-verification": "technical-verification-worker",
    "requirements-discovery": "discovery-worker",
    "improvement-discovery": "discovery-worker",
    "error-analysis": "diagnosis-worker",
    "implementation-option-selection": "direction-selection-worker",
    "implementation-planning": "planning-worker",
}
WORKER_PREAMBLE_FILENAME_BY_AUDIENCE = {
    "analysis": "worker-prompt-preamble.md",
    "implementation-executor": "implementation-worker-preamble.md",
    "implementation-verifier": "implementation-worker-preamble.md",
    "report-writer": "report-writer-prompt-preamble.md",
}
WORKER_ERROR_CONTRACT_FILENAME = "worker-error-contract.md"
# coverage-critic gap 검증 라운드의 dispatch kind. 번호 라운드(`reverify-r<N>`)와
# 같은 검증 계약(audience `reverification-worker`, 렌더러 서명, 출력 계약)을
# 따르되 라운드 원장 밖이라 번호가 없다. 실측(2026-09-09 dev-10642): 이 kind 가
# 없어 gap 검증 프롬프트를 만들 수 없었고 gap 3건 전부 `gapsUnverified` 로 남았다.
CRITIC_VERIFY_DISPATCH_KIND = "critic-verify"
REVERIFY_DISPATCH_KIND_PREFIX = "reverify-r"
# 계획 본문 검증(§5.5.9) 라운드의 dispatch kind. 결과 파일명 규약이 이미
# `<role>-worker-plan-verify-r<N>-…` 이라(`plan-body-verification.md` §"Round
# protocol", `dispatch_state._plan_verify_result_workers`) kind 도 같은 번호를
# 단다. 실측(2026-09-09 dev-10642 implementation-planning 001): 이 kind 가 없어
# 계획 항목 큐를 `reverify-r<N>` 으로 보낼 수밖에 없었고, 그러면 검증 계약이
# `okstra convergence reverify-prompt` 서명을 요구하는데 계획 항목 큐의 정본
# 렌더러는 `okstra plan-items prompt` 라 통과할 값이 하나도 없었다. 라운드가
# 한 번도 열리지 못한 채 `planBodyVerification.roundCount` 가 0 으로 남았다.
PLAN_VERIFY_DISPATCH_KIND_PREFIX = "plan-verify-r"


def _numbered_round(dispatch_kind: str, prefix: str) -> int | None:
    """`<prefix><N>` 의 N. 접두사가 다르거나 N 이 양의 정수가 아니면 None."""
    if not dispatch_kind.startswith(prefix):
        return None
    suffix = dispatch_kind[len(prefix):]
    if not suffix.isdigit() or int(suffix) < 1:
        return None
    return int(suffix)


def is_verification_dispatch_kind(dispatch_kind: str) -> bool:
    """번호 reverify·계획 본문 라운드와 critic gap 검증 — 검증 프롬프트 계약을
    받는 kind."""
    return (
        dispatch_kind.startswith(REVERIFY_DISPATCH_KIND_PREFIX)
        or dispatch_kind.startswith(PLAN_VERIFY_DISPATCH_KIND_PREFIX)
        or dispatch_kind == CRITIC_VERIFY_DISPATCH_KIND
    )


def is_plan_verify_dispatch_kind(dispatch_kind: str) -> bool:
    """계획 본문 검증 라운드인가. 검증 계약이 요구하는 렌더러 서명이 번호
    reverify 와 다르므로 계약 검사가 이 둘을 갈라야 한다."""
    return _numbered_round(dispatch_kind, PLAN_VERIFY_DISPATCH_KIND_PREFIX) is not None


def is_plan_critic_verification(
    *, task_type: str, assignment_ref: str, dispatch_kind: str,
) -> bool:
    return (
        task_type == "implementation-planning"
        and assignment_ref == "critic/scope"
        and is_plan_verify_dispatch_kind(dispatch_kind)
    )


def verification_dispatch_round(dispatch_kind: str) -> int | None:
    """검증 kind 가 적는 라운드 번호. 번호 없는 kind(`critic-verify`)는 1,
    검증 kind 가 아니거나 번호가 깨졌으면 None.

    예약(`agent-prompt materialize`)과 디스패치가 같은 값을 적어야 한다 —
    validate-run 이 team-state 의 kind 와 예약된 invocation 의 `dispatchKind`
    를 대조하므로, 두 계산이 갈리면 그 디스패치가 거부된다."""
    if dispatch_kind == CRITIC_VERIFY_DISPATCH_KIND:
        return 1
    for prefix in (REVERIFY_DISPATCH_KIND_PREFIX, PLAN_VERIFY_DISPATCH_KIND_PREFIX):
        if dispatch_kind.startswith(prefix):
            return _numbered_round(dispatch_kind, prefix)
    return None


@dataclass(frozen=True)
class PromptPlan:
    audience: PromptAudience
    duty_audience: AgentAudience
    equality_group: str | None
    packet_only: bool
    allow_coding_preflight: bool
    required_headers: tuple[str, ...]
    max_body_lines: int | None
    max_directive_lines: int | None
    # 이 audience 의 `## Inputs` 가 승인 계획을 함께 열거하는가. 승인 계획을
    # 기준으로 판정하라고 요구하는 phase 만 참이다 — 없는 phase 에 실으면
    # 열거만 되고 열 파일이 없는 줄이 된다.
    include_approved_plan: bool = False


def resolve_prompt_plan(
    *,
    task_type: str,
    worker_id: str,
    executor_worker_id: str | None,
    dispatch_kind: str,
) -> PromptPlan:
    """Resolve prompt policy without consulting provider or model identity."""
    if task_type not in SUPPORTED_TASK_TYPES:
        raise ValueError(f"unsupported task type: {task_type}")
    if task_type == "release-handoff":
        return _plan("lead-only")
    if not worker_id.strip():
        raise ValueError("worker ID is required")
    if worker_id == "translator" or dispatch_kind == "translator":
        if worker_id != "translator" or dispatch_kind != "translator":
            raise ValueError("translator worker and dispatch kind must match")
        return _plan("translator", duty_audience="translator")
    if is_verification_dispatch_kind(dispatch_kind):
        return _plan("reverify")
    # A critic pass keeps the full analysis contract — worker anchor headers, the
    # audit sidecar, the packet boundary — but is exempt from the equality group.
    # Its body is deliberately unlike the initial one (it asks for coverage gaps
    # and unrequested work, not findings), so grouping it with the initial
    # analysis prompts makes `validate_analysis_prompt_set` compare two prompts
    # that are *supposed* to differ and fail every critic-enabled run.
    analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
    analysis_duty: AgentAudience = (
        critic_duty_for_task_type(task_type)
        if dispatch_kind == "critic"
        else "acceptance-verifier"
        if task_type == "final-verification"
        else ANALYSIS_DUTY_BY_TASK_TYPE.get(task_type, "analysis-worker")
    )
    if task_type == "implementation" and not executor_worker_id:
        raise ValueError("implementation executor worker ID is required")
    if worker_id == "report-writer":
        return _plan("report-writer")
    if task_type == "implementation" and worker_id == executor_worker_id:
        return _plan(
            "implementation-executor",
            allow_coding_preflight=True,
            required_headers=IMPLEMENTATION_HEADERS,
        )
    if task_type == "implementation":
        return _plan(
            "implementation-verifier",
            equality_group="implementation-verifier-core",
            allow_coding_preflight=True,
            required_headers=IMPLEMENTATION_HEADERS,
        )
    if task_type == "final-verification":
        return _plan(
            "analysis",
            duty_audience=analysis_duty,
            equality_group=analysis_equality_group,
            packet_only=True,
            include_approved_plan=True,
            required_headers=FINAL_VERIFICATION_HEADERS,
            max_body_lines=96,
            max_directive_lines=40,
        )
    if task_type == "improvement-discovery":
        return _plan(
            "analysis",
            duty_audience=analysis_duty,
            equality_group=analysis_equality_group,
            packet_only=True,
            required_headers=(GRILLING_LOG_HEADER,),
        )
    return _plan(
        "analysis",
        duty_audience=analysis_duty,
        equality_group=analysis_equality_group,
        packet_only=True,
    )


def resolve_prompt_plan_for_manifest(
    *,
    manifest: Mapping[str, Any],
    worker_id: str,
    dispatch_kind: str,
) -> PromptPlan:
    """Resolve policy from canonical task and executor manifest fields."""
    task_type = _required_string(manifest, "taskType")
    return resolve_prompt_plan(
        task_type=task_type,
        worker_id=worker_id,
        executor_worker_id=_executor_worker_id(manifest),
        dispatch_kind=dispatch_kind,
    )


def _plan(
    audience: PromptAudience,
    *,
    duty_audience: AgentAudience | None = None,
    equality_group: str | None = None,
    packet_only: bool = False,
    include_approved_plan: bool = False,
    allow_coding_preflight: bool = False,
    required_headers: tuple[str, ...] = (),
    max_body_lines: int | None = None,
    max_directive_lines: int | None = None,
) -> PromptPlan:
    return PromptPlan(
        audience=audience,
        duty_audience=duty_audience or _duty_audience(audience),
        equality_group=equality_group,
        packet_only=packet_only,
        include_approved_plan=include_approved_plan,
        allow_coding_preflight=allow_coding_preflight,
        required_headers=_worker_facing_headers(audience, required_headers),
        max_body_lines=max_body_lines,
        max_directive_lines=max_directive_lines,
    )


def _duty_audience(audience: PromptAudience) -> AgentAudience:
    return {
        "analysis": "analysis-worker",
        "implementation-executor": "implementation-executor",
        "implementation-verifier": "implementation-verifier",
        "report-writer": "report-writer",
        "translator": "translator",
        "reverify": "reverification-worker",
        "lead-only": "lead",
    }[audience]


def _worker_facing_headers(
    audience: PromptAudience,
    required_headers: tuple[str, ...],
) -> tuple[str, ...]:
    """Prepend the errors-path pair every prompt that reaches a worker carries."""
    if audience == "lead-only":
        return required_headers
    return (*ERRORS_PATH_HEADERS, *required_headers)


# 하나의 run 이 실제로 디스패치할 수 있는 critic 은 한 종류다. 판정은 task type
# 만의 함수이므로 여기 한 번만 적고, 로스터 등록과 프롬프트 정체성 검사가 같은
# 값을 읽는다 — 로스터가 실행될 수 없는 critic 행을 하나 더 들고 있으면 provider
# 로만 만든 role 라벨이 겹쳐 validate-run 의 중복 역할 검사에 걸린다.
CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT: dict[str, AgentAudience] = {
    "scope": "scope-critic",
    "acceptance": "acceptance-critic",
}


def critic_duty_for_task_type(task_type: str) -> AgentAudience:
    """이 task type 의 critic 직무."""
    return (
        "acceptance-critic"
        if task_type == "final-verification"
        else "scope-critic"
    )


def critic_assignment_ref(task_type: str) -> str:
    """이 task type 이 등록할 유일한 critic 배정 참조."""
    duty = critic_duty_for_task_type(task_type)
    segment = next(
        key
        for key, value in CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.items()
        if value == duty
    )
    return f"critic/{segment}"


def _executor_worker_id(manifest: Mapping[str, Any]) -> str | None:
    """실행자의 **worker_id**. provider 이름이 아니다.

    한 provider 가 실행자와 검증자를 겸하면 슬롯마다 역할이 붙어 실행자의 id 는
    `codex-implementer` 가 된다. 여기서 provider(`codex`)를 돌려주면 실행자가
    자기 자신과 매칭되지 않아 검증자 프롬프트로 실체화되고, 그 사실은
    `agent invocation duty does not match expected audience` 로 디스패치 단계에서야
    드러난다.
    """
    team_contract = manifest.get("teamContract")
    if not isinstance(team_contract, Mapping):
        return None
    executor = team_contract.get("executor")
    if not isinstance(executor, Mapping):
        return None
    worker_id = executor.get("workerId")
    return worker_id.strip() if isinstance(worker_id, str) and worker_id.strip() else None


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