"""다음 Phase 포인터 값의 shape · 승격 규칙.

`{phase, status, rationale}` 구조체를 만들고, 판별하고, 구형 문자열에서
승격시키는 것까지만 소유한다. 다음 phase 를 **계산**하는 일은 이 모듈이
하지 않는다 — 그것은 `okstra_ctl.next_phase` 의 몫이고, 그 모듈이 여기의
심볼을 재노출한다.

왜 okstra_ctl 이 아니라 여기 있는가:

읽기측 상태 접근자 `okstra_project.state` 가 매니페스트를 돌려줄 때마다
포인터를 승격시켜야 한다. 그런데 `okstra_ctl` 의 어느 모듈을 import 하든
`okstra_ctl/__init__.py` 가 먼저 실행되고, 그 `__init__` 은 `.ids` 를 거쳐
`okstra_project.state` 의 `slugify` 를 도로 끌어온다. 즉 이 값 타입을
`okstra_ctl` 안에 두면 leaf 로 만들어도 패키지 `__init__` 을 통해 순환이
되살아난다. 그래서 하위 계층이 소유하고 상위 계층이 재노출한다.

이 모듈은 okstra 안의 어떤 것도 import 하지 않는다. 그 성질이 위 설명의
전제이므로 import 를 추가하기 전에 순환이 되살아나지 않는지 확인할 것.
"""
from __future__ import annotations

from typing import Any, Mapping

STATUS_READY = "ready"
STATUS_PENDING = "pending"
STATUS_BLOCKED = "blocked"
STATUS_TERMINAL = "terminal"

POINTER_STATUSES = frozenset(
    {STATUS_READY, STATUS_PENDING, STATUS_BLOCKED, STATUS_TERMINAL}
)

# 구형 문자열 포인터가 쓰던 센티널 → status. 이 표는 승격에만 쓰이며
# 새 포인터를 만드는 데는 절대 쓰지 않는다.
LEGACY_SENTINELS = {
    "": STATUS_PENDING,
    "unknown": STATUS_PENDING,
    "pending-routing-decision": STATUS_PENDING,
    "pending-release-handoff": STATUS_PENDING,
    "done-or-follow-up": STATUS_TERMINAL,
}


def make(
    phase: str = "", status: str = STATUS_PENDING, rationale: str = ""
) -> dict[str, str]:
    """포인터 구조체 하나를 만든다. status 가 허용 집합 밖이면 거부한다."""
    if status not in POINTER_STATUSES:
        raise ValueError(f"unknown next-phase status: {status!r}")
    return {"phase": phase, "status": status, "rationale": rationale}


def is_pointer(value: Any) -> bool:
    """value 가 유효한 포인터 구조체인지."""
    return (
        isinstance(value, Mapping)
        and isinstance(value.get("phase"), str)
        and value.get("status") in POINTER_STATUSES
        and isinstance(value.get("rationale"), str)
    )


def promote(value: Any) -> dict[str, str]:
    """구형 문자열·None 을 포인터로 올린다.

    이미 포인터인 입력은 같은 값을 담은 새 dict 로 돌려준다 — 입력을 그대로
    돌려주지 않는다. 호출부가 반환값을 그 자리에서 변형해도 manifest 원본이
    오염되지 않아야 하기 때문이다.

    pre-v1 무호환 원칙의 의도적 예외다. 근거는 업그레이드를 가로지르는
    진행 중 태스크가 영구히 막히는 기존 사례 — ADR-0004 Consequences.
    """
    if is_pointer(value):
        return {
            "phase": value["phase"],
            "status": value["status"],
            "rationale": value["rationale"],
        }
    if not isinstance(value, str):
        return make()
    text = value.strip()
    if text in LEGACY_SENTINELS:
        return make(status=LEGACY_SENTINELS[text])
    return make(phase=text, status=STATUS_READY)
