"""명령이 받은 경로를 해석하고, 허용된 것만 돌려준다.

이 CLI 는 모델이 부르는 표면이라 경로 인자를 그대로 믿을 수 없다. 여기 있는
함수들은 전부 같은 모양이다 — 받은 문자열을 절대 경로로 만들고, 프로젝트
루트(또는 명시된 상위) 안에 있는지 확인하고, 벗어나면 거절한다. 어느 것도
okstra 의 phase 나 role 을 모른다.

`AgentPromptCliError` 도 여기 있다. 모든 층이 이 하나로 실패를 알린다.
"""
from __future__ import annotations

from pathlib import Path
from pathlib import PurePosixPath
from typing import Any, Mapping

from ...json_boundary import JsonBoundaryError, load_owned_object



class AgentPromptCliError(RuntimeError):
    """Raised when CLI input cannot become a valid invocation request."""


def _authorized_path(
    project_root: Path,
    raw_path: str,
    raw_roots: object,
    label: str,
    *,
    must_exist: bool,
) -> Path:
    if not isinstance(raw_roots, list) or not raw_roots:
        raise AgentPromptCliError(f"run manifest has no authorized {label} roots")
    path = _candidate(project_root, raw_path, must_exist=must_exist, label=label)
    roots = [
        _project_manifest_path(project_root, value, f"{label} root", must_exist=True)
        for value in raw_roots
    ]
    if not any(_is_relative_to(path, root) for root in roots):
        # 루트를 말하지 않으면 시행착오로 찾는다(2026-09-09 실측: ledger 의
        # baseNarrativePath 를 `state/` 에 두었다가 거부돼 `worker-results/` 로
        # 옮겨서야 통과).
        listed = ", ".join(str(value) for value in raw_roots)
        raise AgentPromptCliError(
            f"{label} path is outside authorized roots: {path}; authorized "
            f"{label} roots (project-relative): {listed}"
        )
    return path


def _candidate(project_root: Path, raw: str, *, must_exist: bool, label: str) -> Path:
    candidate = Path(raw)
    if not candidate.is_absolute():
        candidate = project_root / candidate
    try:
        if must_exist or candidate.exists() or candidate.is_symlink():
            return candidate.resolve(strict=True)
        return candidate.parent.resolve(strict=True) / candidate.name
    except OSError as exc:
        raise AgentPromptCliError(f"{label} path is invalid: {candidate}") from exc


def _project_input(project_root: Path, raw: str, label: str) -> Path:
    path = _candidate(project_root, raw, must_exist=True, label=label)
    if not _is_relative_to(path, project_root):
        raise AgentPromptCliError(f"{label} path escapes project root: {path}")
    return path


def _project_manifest_path(
    project_root: Path,
    value: object,
    label: str,
    *,
    must_exist: bool,
) -> Path:
    if not isinstance(value, str) or not value:
        raise AgentPromptCliError(f"{label} path is invalid")
    pure = PurePosixPath(value)
    if pure.is_absolute() or any(part in {"", ".", ".."} for part in pure.parts):
        raise AgentPromptCliError(f"{label} path is invalid")
    path = _candidate(project_root, value, must_exist=must_exist, label=label)
    if not _is_relative_to(path, project_root):
        raise AgentPromptCliError(f"{label} path escapes project root: {path}")
    return path


def _exact_path(raw: str, expected: Path, must_exist: bool, label: str) -> Path:
    actual = (
        Path(raw).resolve(strict=True)
        if must_exist
        else Path(raw).parent.resolve(strict=True) / Path(raw).name
    )
    expected_actual = (
        expected.resolve(strict=True)
        if must_exist
        else expected.parent.resolve(strict=True) / expected.name
    )
    if actual != expected_actual:
        raise AgentPromptCliError(f"{label} does not use canonical standalone path")
    return actual


def _project_root(value: str) -> Path:
    try:
        return Path(value).resolve(strict=True)
    except OSError as exc:
        raise AgentPromptCliError(f"project root not found: {value}") from exc


def _relative(project_root: Path, path: Path) -> str:
    try:
        return path.relative_to(project_root).as_posix()
    except ValueError as exc:
        raise AgentPromptCliError(f"path escapes project root: {path}") from exc


def _read_json_object(path: Path, label: str) -> dict[str, Any]:
    try:
        value = load_owned_object(path, artifact="agent prompt metadata")
    except JsonBoundaryError as exc:
        raise AgentPromptCliError(f"{label} is invalid: {path}") from exc
    if not isinstance(value, dict):
        raise AgentPromptCliError(f"{label} must be a JSON object: {path}")
    return value


def _mapping(value: object, label: str) -> Mapping[str, Any]:
    if not isinstance(value, Mapping):
        raise AgentPromptCliError(f"{label} is missing or invalid")
    return value


def _is_relative_to(path: Path, root: Path) -> bool:
    try:
        path.relative_to(root)
    except ValueError:
        return False
    return True


def _is_slug(value: str) -> bool:
    if not value or value.startswith("-") or value.endswith("-"):
        return False
    return all(part.isalnum() and part.lower() == part for part in value.split("-"))
