"""anu_v3.dispatch_profile_selection — TRACK A: profile engine → dispatch
selection 연결 seam (task-2553+38).

회장 GO 병렬 4트랙 Track A. +33 C1 engine(`anu_v3.policy_profile_engine`,
정본 API parse_goal_request→resolve_policy, byte-0)을 **실 운영 dispatch
selection 경로에 연결**하는 신규 additive 결선 모듈.

9-R.1 (본문 우선):
  본 모듈 **자체가 운영 dispatch selection 연결 seam** 이다 — callable
  entrypoint(`select_profile_for_dispatch` / `run_dispatch_profile_selection`)
  + 파일레벨 contract. +37 `normal_completion_callback_collector_entrypoint`
  선례와 동일 패턴: 신규 seam 모듈이 곧 연결점이며, 기존 frozen/tracked
  dispatch 코드 in-place 편집은 불요하다. "연결" 달성 = seam 모듈 존재 +
  입출력 contract + regression 으로 dispatch→engine 경로 입증. 기존 dispatch
  경로가 이 seam 을 호출하도록 하는 실 채택(in-place adoption)은 별도
  운영단계 (본 task 는 seam+contract+regression 으로 결선 완성).

본 모듈 한정 책임 (순수, 부작용 0 — 파일 쓰기 0, network 0, git 0,
GitHub API 0, 실 dispatch 실행 0):
  - dispatch selection 요청(goal_type + policy_profile + boundary) 정규화
  - C1 engine **read-only 소비**: parse_goal_request → resolve_policy
    (정본 API. engine import 만, engine 객체 mutation 0, engine byte-0)
  - PolicyResolution → dispatch 가 그대로 소비할 selection binding 산출
  - profile 부재 / mismatch / engine HOLD → **안전 거부(자동 적용 0)**:
    예외를 dispatch lifecycle 로 전파하지 않고 SELECTION_REFUSED /
    HOLD_FOR_CHAIR 결정 dict 로 fail-closed 변환 (dispatch 무파괴)
  - decision/result JSON 산출 (DispatchProfileSelection.to_decision_dict)

설계 invariant:
  - 신규 별도 모듈 (strict-additive). 기존 anu_v3 tracked 파일·engine·
    frozen anchor·+22~+37 원본 import-only / mutation 0.
  - engine 은 `anu_v3.policy_profile_engine` 정본 API 만 호출 (parse_goal_
    request, resolve_policy, PolicyEngineError). engine 내부 재구현 0.
  - seam 은 dispatch 를 **선택만** 하고 **실행하지 않는다** —
    `DISPATCH_LIFECYCLE_EFFECT = "none"`. 실 운영 적용은 결선 후 별도.
  - 외부 의존성 0 (engine 과 동일 — offline 100%).
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Final, Mapping, Sequence

from anu_v3.policy_profile_engine import (
    PROFILE_JSON_DIR_DEFAULT,
    PROFILE_SCHEMA_DIR_DEFAULT,
    SCHEMA_DIR_DEFAULT,
    PolicyEngineError,
    PolicyResolution,
    parse_goal_request,
    resolve_policy,
)

SEAM_MODULE: Final[str] = "anu_v3.dispatch_profile_selection"
SEAM_VERSION: Final[str] = "task-2553+38.A.v1"
SELECTION_SCHEMA_ID: Final[str] = "anu_v3.dispatch_profile_selection.v1"

# 본 seam 은 profile 을 **선택**만 한다 — 실 dispatch/PR/merge 실행 0.
# (회장 §5 / 9-R.1 — 실 운영 적용은 결선 후 별도 운영단계.)
DISPATCH_LIFECYCLE_EFFECT: Final[str] = "none"

# 결정 status (fail-closed 3-상태).
STATUS_SELECTED: Final[str] = "SELECTED"
STATUS_REFUSED: Final[str] = "SELECTION_REFUSED"
STATUS_HOLD: Final[str] = "HOLD_FOR_CHAIR"

# 요청 정규화 단계 자체 거부 코드 (engine 진입 이전 fail-closed).
REFUSAL_REQUEST_NOT_MAPPING: Final[str] = "selection_request_not_mapping"
REFUSAL_PROFILE_NAME_MISSING: Final[str] = "selection_policy_profile_name_missing"


class DispatchProfileSelectionError(ValueError):
    """seam 요청 정규화 실패. dispatch lifecycle 로 전파하지 않고 내부에서
    SELECTION_REFUSED 로 변환된다 (안전 거부 — 자동 적용 0)."""

    def __init__(self, code: str, message: str) -> None:
        super().__init__(f"[{code}] {message}")
        self.code = code
        self.message = message


@dataclass
class DispatchSelectionRequest:
    """dispatch 가 seam 에 넘기는 입력 (goal_type + policy_profile + boundary)."""

    goal_id: str
    goal_statement: str
    policy_profile_name: str
    goal_type: str | None = None
    boundary: list[str] = field(default_factory=list)

    def to_goal_request(self) -> dict[str, Any]:
        """C1 engine 정본 입력(goal_request_2553plus33.schema.json) 형태."""
        gr: dict[str, Any] = {
            "goal_id": self.goal_id,
            "goal_statement": self.goal_statement,
            "boundary": list(self.boundary),
            "policy_profile": {"name": self.policy_profile_name},
        }
        if self.goal_type:
            gr["goal_type"] = self.goal_type
        return gr


def build_selection_request(raw: Any) -> DispatchSelectionRequest:
    """dispatch 가 넘긴 raw dict → DispatchSelectionRequest (fail-closed).

    engine 진입 *이전* 의 정규화. 형태 불량은
    DispatchProfileSelectionError 로 raise 하되, seam entrypoint 가 이를
    SELECTION_REFUSED 안전 거부로 흡수한다 (dispatch 무파괴).
    """
    if not isinstance(raw, Mapping):
        raise DispatchProfileSelectionError(
            REFUSAL_REQUEST_NOT_MAPPING,
            f"selection request dict 아님: {type(raw).__name__}",
        )
    pp = raw.get("policy_profile") or {}
    name = pp.get("name") if isinstance(pp, Mapping) else None
    if not name or not isinstance(name, str):
        raise DispatchProfileSelectionError(
            REFUSAL_PROFILE_NAME_MISSING, "policy_profile.name 누락/무효"
        )
    gt = raw.get("goal_type")
    return DispatchSelectionRequest(
        goal_id=str(raw.get("goal_id", "")),
        goal_statement=str(raw.get("goal_statement", "")),
        policy_profile_name=name,
        goal_type=gt.strip() if isinstance(gt, str) and gt.strip() else None,
        boundary=[str(b) for b in (raw.get("boundary") or [])],
    )


@dataclass
class DispatchProfileSelection:
    """seam 산출 — dispatch 가 그대로 소비할 profile selection binding."""

    status: str  # SELECTED | SELECTION_REFUSED | HOLD_FOR_CHAIR
    profile_bound: bool  # dispatch 에 바인딩 가능한 profile 확정 여부
    auto_apply: bool  # 자동 적용 허용 (부재·mismatch·HOLD → 항상 False)
    goal_id: str
    goal_type: str
    profile_id: str
    profile_version: str
    boundary: dict[str, Any]
    gate_condition_names: list[str]
    hold_trigger_conditions: list[str]
    allowed_actions: list[str]
    forbidden_actions: list[str]
    completion_packet_meta_ref: str | None
    evidence_meta_ref: str | None
    refusal_code: str | None = None
    refusal_reason: str | None = None
    dispatch_lifecycle_effect: str = DISPATCH_LIFECYCLE_EFFECT
    seam: str = SEAM_MODULE
    seam_version: str = SEAM_VERSION
    engine_decision: dict[str, Any] | None = None

    def to_decision_dict(self) -> dict[str, Any]:
        return {
            "schema": SELECTION_SCHEMA_ID,
            "seam": self.seam,
            "seam_version": self.seam_version,
            "status": self.status,
            "profile_bound": self.profile_bound,
            "auto_apply": self.auto_apply,
            "dispatch_lifecycle_effect": self.dispatch_lifecycle_effect,
            "goal_id": self.goal_id,
            "goal_type": self.goal_type,
            "profile_id": self.profile_id,
            "profile_version": self.profile_version,
            "boundary": self.boundary,
            "gate_condition_names": list(self.gate_condition_names),
            "hold_trigger_conditions": list(self.hold_trigger_conditions),
            "allowed_actions": list(self.allowed_actions),
            "forbidden_actions": list(self.forbidden_actions),
            "completion_packet_meta_ref": self.completion_packet_meta_ref,
            "evidence_meta_ref": self.evidence_meta_ref,
            "refusal_code": self.refusal_code,
            "refusal_reason": self.refusal_reason,
            "engine_decision": self.engine_decision,
        }

    def selection_binding(self) -> dict[str, Any]:
        """dispatch 소비용 평면 binding (SELECTED 시에만 의미 — 그 외 빈 contract)."""
        return {
            "status": self.status,
            "profile_bound": self.profile_bound,
            "auto_apply": self.auto_apply,
            "goal_id": self.goal_id,
            "goal_type": self.goal_type,
            "profile_id": self.profile_id,
            "gate_condition_names": list(self.gate_condition_names),
            "hold_trigger_conditions": list(self.hold_trigger_conditions),
            "allowed_actions": list(self.allowed_actions),
            "forbidden_actions": list(self.forbidden_actions),
            "completion_packet_meta_ref": self.completion_packet_meta_ref,
            "evidence_meta_ref": self.evidence_meta_ref,
        }


def _refused(
    code: str, reason: str, *, status: str = STATUS_REFUSED
) -> DispatchProfileSelection:
    """안전 거부 결정 — profile 미바인딩·자동 적용 0, dispatch 무파괴."""
    return DispatchProfileSelection(
        status=status,
        profile_bound=False,
        auto_apply=False,
        goal_id="",
        goal_type="",
        profile_id="",
        profile_version="",
        boundary={},
        gate_condition_names=[],
        hold_trigger_conditions=[],
        allowed_actions=[],
        forbidden_actions=[],
        completion_packet_meta_ref=None,
        evidence_meta_ref=None,
        refusal_code=code,
        refusal_reason=reason,
    )


def _from_resolution(res: PolicyResolution) -> DispatchProfileSelection:
    """PolicyResolution → dispatch selection.

    engine status RESOLVED → SELECTED(자동 적용 허용). HOLD_FOR_CHAIR →
    HOLD(자동 적용 0, profile 미바인딩 — fail-closed). engine read-only:
    res 는 resolve_policy 가 반환한 객체이며 본 함수는 읽기만 한다.
    """
    binding = res.to_coordinator_binding()  # engine 정본 어댑터 (read-only)
    decision = res.to_decision_dict()
    if res.status == "HOLD_FOR_CHAIR":
        return DispatchProfileSelection(
            status=STATUS_HOLD,
            profile_bound=False,
            auto_apply=False,
            goal_id=res.goal_id,
            goal_type=res.goal_type,
            profile_id=res.profile_id,
            profile_version=res.profile_version,
            boundary=res.boundary,
            gate_condition_names=list(binding["gate_condition_names"]),
            hold_trigger_conditions=list(binding["hold_trigger_conditions"]),
            allowed_actions=list(binding["allowed_actions"]),
            forbidden_actions=list(binding["forbidden_actions"]),
            completion_packet_meta_ref=binding["completion_packet_meta_ref"],
            evidence_meta_ref=binding["evidence_meta_ref"],
            refusal_code="engine_hold_for_chair",
            refusal_reason=res.hold_reason,
            engine_decision=decision,
        )
    return DispatchProfileSelection(
        status=STATUS_SELECTED,
        profile_bound=True,
        auto_apply=True,
        goal_id=res.goal_id,
        goal_type=res.goal_type,
        profile_id=res.profile_id,
        profile_version=res.profile_version,
        boundary=res.boundary,
        gate_condition_names=list(binding["gate_condition_names"]),
        hold_trigger_conditions=list(binding["hold_trigger_conditions"]),
        allowed_actions=list(binding["allowed_actions"]),
        forbidden_actions=list(binding["forbidden_actions"]),
        completion_packet_meta_ref=binding["completion_packet_meta_ref"],
        evidence_meta_ref=binding["evidence_meta_ref"],
        engine_decision=decision,
    )


def select_profile_for_dispatch(
    request: DispatchSelectionRequest,
    *,
    profile_json_dir: str | Path = PROFILE_JSON_DIR_DEFAULT,
    profile_schema_dir: str | Path = PROFILE_SCHEMA_DIR_DEFAULT,
    schema_dir: str | Path = SCHEMA_DIR_DEFAULT,
) -> DispatchProfileSelection:
    """**연결 seam entrypoint** — dispatch 시 goal_type+policy_profile+
    boundary 로 profile 을 자동 선택·로딩한다.

    C1 engine 정본 API 를 read-only 로 소비: parse_goal_request 로 요청을
    검증한 뒤 resolve_policy 로 gate/HOLD/allowed/forbidden/evidence/
    completion-packet 을 산출한다. engine mutation 0.

    profile 부재 / schema mismatch / engine HOLD 는 예외를 dispatch 로
    전파하지 않고 **안전 거부(자동 적용 0)** 결정으로 fail-closed 변환한다
    — dispatch lifecycle 무파괴 (`DISPATCH_LIFECYCLE_EFFECT == "none"`).
    """
    goal_request = request.to_goal_request()
    try:
        # 정본 API #1 — goal_request parser (정적 meta-schema, fail-closed).
        parse_goal_request(goal_request, schema_dir=schema_dir)
        # 정본 API #2 — END-TO-END resolver (read-only consume).
        res = resolve_policy(
            goal_request,
            profile_json_dir=profile_json_dir,
            profile_schema_dir=profile_schema_dir,
            schema_dir=schema_dir,
        )
    except PolicyEngineError as e:
        # profile 부재·mismatch·schema 실패 → 안전 거부 (자동 적용 0).
        return _refused(e.code, e.message)
    return _from_resolution(res)


def run_dispatch_profile_selection(
    raw_request: Any,
    *,
    profile_json_dir: str | Path = PROFILE_JSON_DIR_DEFAULT,
    profile_schema_dir: str | Path = PROFILE_SCHEMA_DIR_DEFAULT,
    schema_dir: str | Path = SCHEMA_DIR_DEFAULT,
) -> dict[str, Any]:
    """파일레벨 contract entrypoint — raw dict → decision dict.

    dispatch 운영 경로가 단일 호출로 소비할 수 있는 결선점. 요청 정규화
    실패도 SELECTION_REFUSED 안전 거부로 흡수한다 (예외 비전파).
    """
    try:
        req = build_selection_request(raw_request)
    except DispatchProfileSelectionError as e:
        return _refused(e.code, e.message).to_decision_dict()
    sel = select_profile_for_dispatch(
        req,
        profile_json_dir=profile_json_dir,
        profile_schema_dir=profile_schema_dir,
        schema_dir=schema_dir,
    )
    return sel.to_decision_dict()


def selection_requests_from_fixture(cases: Sequence[Mapping[str, Any]]) -> list[Any]:
    """fixture case[].request 목록 추출 (regression/dry-run 공용)."""
    return [dict(c["request"]) for c in cases]


__all__ = [
    "SEAM_MODULE",
    "SEAM_VERSION",
    "SELECTION_SCHEMA_ID",
    "DISPATCH_LIFECYCLE_EFFECT",
    "STATUS_SELECTED",
    "STATUS_REFUSED",
    "STATUS_HOLD",
    "REFUSAL_REQUEST_NOT_MAPPING",
    "REFUSAL_PROFILE_NAME_MISSING",
    "DispatchProfileSelectionError",
    "DispatchSelectionRequest",
    "DispatchProfileSelection",
    "build_selection_request",
    "select_profile_for_dispatch",
    "run_dispatch_profile_selection",
    "selection_requests_from_fixture",
]
