"""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({
    "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] = {
    "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"


@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


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 dispatch_kind.startswith("reverify-r"):
        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,
            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,
    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,
        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:
    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
    provider = executor.get("provider")
    return provider.strip() if isinstance(provider, str) and provider.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()
