"""야외 구조물 파이프 정책 SOT — 조합 유효성의 단일 소유자.

설계 = docs/superpowers/specs/
2026-07-31-structure-skeleton-look-and-place-mark-production-merge-design.md §5.1

## 왜 중앙 SOT 인가

플래그를 boolean 으로 늘리면 조합 수가 곱으로 늘고, 유효성 판단이
`config_hash` 나 manifest applicability 로 새어 나간다. **hash 는 무효화·
감사 장치이지 validity gate 가 아니다** — hash 가 달라도 실행은 그대로
되고, 불가능한 조합은 조용히 no-op 이 되거나 중간에서 터진다.

그래서 유효성은 이 모듈 하나가 소유한다. 스텝은 여기서 검증된 객체를
받아 쓰고, manifest applicability 는 이 객체에서 파생하며, 각 스텝의
config_hash 는 `as_hash_payload()` 를 **기록**만 한다.
"""
from __future__ import annotations

import dataclasses
from typing import Any, Dict, List, Optional

# 정책 계약 자체의 버전 — 값의 의미가 바뀌면 올린다(각 스텝 hash 에 실린다).
POLICY_VERSION = "1"

SEED_RECIPE_LEGACY = "legacy"
SEED_RECIPE_SKELETON_LOOK_V1 = "skeleton_look_v1"
PLACE_MARK_OFF = "off"
PLACE_MARK_V1 = "v1"
CRITIQUE_OFF = "off"
CRITIQUE_STRUCTURE_PRESERVING_V1 = "structure_preserving_v1"

_SEED_RECIPES = (SEED_RECIPE_LEGACY, SEED_RECIPE_SKELETON_LOOK_V1)
_PLACE_MARK_MODES = (PLACE_MARK_OFF, PLACE_MARK_V1)
_CRITIQUE_MODES = (CRITIQUE_OFF, CRITIQUE_STRUCTURE_PRESERVING_V1)


@dataclasses.dataclass(frozen=True)
class StructurePipelinePolicy:
    """검증을 통과한 정책. 생성 경로는 resolve_* 하나뿐이다."""

    seed_recipe: str
    place_mark_mode: str
    seed_critique_mode: str

    @property
    def skeleton_look_on(self) -> bool:
        return self.seed_recipe == SEED_RECIPE_SKELETON_LOOK_V1

    @property
    def place_mark_on(self) -> bool:
        return self.place_mark_mode == PLACE_MARK_V1

    @property
    def structure_critique_on(self) -> bool:
        return self.seed_critique_mode == CRITIQUE_STRUCTURE_PRESERVING_V1

    def as_hash_payload(self) -> Dict[str, Any]:
        """각 스텝 config_hash 가 기록할 정책 스탬프."""
        return {
            "policy_version": POLICY_VERSION,
            "seed_recipe": self.seed_recipe,
            "place_mark_mode": self.place_mark_mode,
            "seed_critique_mode": self.seed_critique_mode,
        }


def _enum(value: Any, allowed: tuple, field: str, violations: List[str]) -> str:
    """미지 값을 기본값으로 흡수하지 않는다 — 오타가 조용한 OFF 가 된다."""
    s = str(value or "").strip()
    if s not in allowed:
        violations.append(
            f"{field}={s!r} 는 허용되지 않는다 (허용: {', '.join(allowed)})")
    return s


def resolve_structure_pipeline_policy(
    settings_obj: Optional[Any] = None,
) -> StructurePipelinePolicy:
    """settings 를 검증된 정책으로 해석한다. 불가능한 조합은 AppError.

    ★실행·claim **이전에** 부른다. 스텝 안에서 늦게 부르면 이미 락을 잡고
    부분 산출을 남긴 뒤에 터진다.
    """
    from app.core.errors import AppError

    if settings_obj is None:
        from app.core.config import settings as settings_obj  # noqa: PLW0127

    violations: List[str] = []
    recipe = _enum(getattr(settings_obj, "structure_seed_recipe", ""),
                   _SEED_RECIPES, "structure_seed_recipe", violations)
    place_mark = _enum(getattr(settings_obj, "place_mark_mode", ""),
                       _PLACE_MARK_MODES, "place_mark_mode", violations)
    critique = _enum(getattr(settings_obj, "seed_critique_mode", ""),
                     _CRITIQUE_MODES, "seed_critique_mode", violations)

    if recipe == SEED_RECIPE_SKELETON_LOOK_V1:
        # 스케치는 저작된 conformance brief 에서 나온다 — 저작이 없으면
        # 입력 자체가 없다.
        if not getattr(settings_obj, "structure_seed_variants_enabled", False):
            violations.append(
                "structure_seed_recipe=skeleton_look_v1 인데 "
                "structure_seed_variants_enabled 가 꺼져 있다 — 스케치의 "
                "입력인 저작 브리프가 생성되지 않는다")
        # 야외 파이프가 꺼져 있으면 스텝 전체가 not-applicable 이라 레시피가
        # 조용히 죽는다.
        for flag in ("outdoor_lane_pipe_enabled", "outdoor_lane_plan_enabled"):
            if not getattr(settings_obj, flag, False):
                violations.append(
                    f"structure_seed_recipe=skeleton_look_v1 인데 {flag} 가 "
                    "꺼져 있다 — 야외 스텝이 not-applicable 이라 레시피가 "
                    "적용되지 않는다")

    if place_mark == PLACE_MARK_V1 and recipe != SEED_RECIPE_SKELETON_LOOK_V1:
        violations.append(
            "place_mark_mode=v1 은 structure_seed_recipe=skeleton_look_v1 을 "
            "요구한다 — 목표 문안이 seed_plan 산출이다")

    if (critique == CRITIQUE_STRUCTURE_PRESERVING_V1
            and recipe != SEED_RECIPE_SKELETON_LOOK_V1):
        violations.append(
            "seed_critique_mode=structure_preserving_v1 은 "
            "structure_seed_recipe=skeleton_look_v1 을 요구한다")

    if violations:
        raise AppError(
            code="structure_pipeline.policy_invalid",
            message="구조 파이프 정책 조합이 유효하지 않다: "
                    + "; ".join(violations),
            status_code=422,
        )
    return StructurePipelinePolicy(
        seed_recipe=recipe,
        place_mark_mode=place_mark,
        seed_critique_mode=critique,
    )
