"""okstra runtime asset verification + project settings provisioning.

okstra 가 깔아둔 런타임(`~/.okstra/lib/python`, `~/.okstra/bin`,
`~/.okstra/version`) 이 있는지 확인하고, 누락 시 InstallationError 로
surface 한다. 또한 대상 프로젝트의 `.claude/settings.local.json` 을
`~/.okstra/templates/settings.local.json` 으로 가리키는 symlink 로
provision 해서, host Claude Code 세션이 같은 프로젝트에서 일하는 동안
okstra worker wrapper 호출이 자동 허용되도록 한다.
"""
from __future__ import annotations

import os
import time
from pathlib import Path
from typing import Optional

from okstra_project.dirs import okstra_home


class InstallationError(Exception):
    """okstra 가 깔아둔 런타임 자산이 누락됨."""


class SettingsLinkError(Exception):
    """`<project>/.claude/settings.local.json` symlink provisioning 실패."""


def installed_version() -> str:
    """Read the version stamp written by `okstra install` to `~/.okstra/version`.

    Returns an empty string if the stamp is missing or unreadable. Callers use
    the result to label generated artifacts (run manifests, final reports) so
    that consumers can tell which okstra release produced a given run — and
    so that report readers can distinguish behaviour drift across upgrades
    without having to dig through git history.

    The stamp lives at `okstra_home() / "version"`. `OKSTRA_HOME` overrides
    the home directory for tests.
    """
    version_file = okstra_home() / "version"
    try:
        return version_file.read_text(encoding="utf-8").strip()
    except OSError:
        return ""


def required_install_paths() -> list[Path]:
    """okstra install 이 채워야 하는 최소 자산 경로.

    `installed_version()` 과 동일하게 `okstra_home()` 을 기준으로 한다 —
    `OKSTRA_HOME` 으로 홈을 격리한 테스트는 conftest 가 같은 자산을 시드해
    설치 검사를 통과시킨다(개발 머신의 실제 `~/.okstra` 설치 여부와 무관).
    """
    home = okstra_home()
    return [
        home / "lib" / "python" / "okstra_project",
        home / "lib" / "python" / "okstra_ctl",
        home / "bin" / "okstra.sh",
        home / "version",
    ]


def verify_installation(workspace_root: Path) -> None:
    """누락된 자산이 있으면 `InstallationError` 를 raise. 메시지에 install
    명령을 포함한다.

    workspace_root 는 prompts/, templates/, validators/, agents/ 를 담은
    디렉터리(`<okstra-package>/runtime` 또는 dev-link 모드의 repo 루트)다.
    이 검사는 ~/.okstra 자산만을 본다; workspace_root 의 존재는 별도 검증.

    `OKSTRA_SKIP_INSTALL_CHECK=1` 이면 설치 자산 검사를 건너뛴다 — 격리
    OKSTRA_HOME 으로 subprocess 를 띄우는 테스트용 게이트(conftest 가 설정,
    OKSTRA_CTL_SKIP_RECONCILE/BACKFILL 과 같은 패턴). workspace_root 검증은
    게이트와 무관하게 항상 수행한다.
    """
    if os.environ.get("OKSTRA_SKIP_INSTALL_CHECK", "").strip():
        missing = []
    else:
        missing = [p for p in required_install_paths() if not p.exists()]
    if missing:
        msg_lines = [f"okstra runtime missing: {p}" for p in missing]
        msg_lines.append("")
        msg_lines.append("okstra has not been installed yet. Run once:")
        msg_lines.append("  npx okstra@latest install")
        raise InstallationError("\n".join(msg_lines))

    workspace_root = Path(workspace_root)
    if not workspace_root.is_dir():
        raise InstallationError(
            f"okstra workspace not found: {workspace_root}\n"
            "Ensure 'okstra paths --field workspace' resolves to an existing directory."
        )


def cleanup_obsolete_generated_docs(
    *, project_root: Path, instruction_set_dir: Path,
) -> None:
    """과거 위치에 남아 있을 수 있는 deprecated 문서들을 best-effort 삭제."""
    project_root = Path(project_root)
    legacy_root = project_root / ".project-docs" / "ai"
    for rel in (
        "claude-skill-index.md",
        "okstra/okstra-guide.md",
        "okstra/worker-catalog.md",
    ):
        target = legacy_root / rel
        if target.exists():
            try:
                target.unlink()
            except OSError:
                pass
    obsolete = Path(instruction_set_dir) / "okstra-skill.md"
    if obsolete.exists():
        try:
            obsolete.unlink()
        except OSError:
            pass
    okstra_dir = legacy_root / "okstra"
    if okstra_dir.is_dir():
        try:
            okstra_dir.rmdir()
        except OSError:
            pass


def installed_settings_template_path() -> Path:
    """okstra install 이 만들어 둔 settings.local.json template 의 절대경로."""
    return okstra_home() / "templates" / "settings.local.json"


def ensure_project_settings_symlink(*, project_root: Path) -> Optional[Path]:
    """`<project_root>/.claude/settings.local.json` 을
    `~/.okstra/templates/settings.local.json` 으로 가리키는 symlink 로
    provisioning 한다.

    Claude Code 가 그 프로젝트에서 host 세션으로 실행될 때 이 파일을
    자동으로 로드하므로, okstra worker wrapper 호출(`okstra-codex-exec.sh`,
    `okstra-antigravity-exec.sh`) 이 별도 `--settings` 인자 없이도 허용된다.

    반환값:
      - target Path: symlink 가 새로 생성되었거나 이미 올바른 위치를
        가리키고 있을 때.
      - None: install 이 아직 settings template 을 깔지 않았을 때
        (구버전 okstra install 등). 상위에서 경고로 흘려보낸다.

    상위 호출자는 `SettingsLinkError` 만 처리하면 된다 — symlink target
    의 dangling 여부, regular 파일 충돌, 사용자가 직접 만든 다른
    symlink 등 의도된 boundary error 만 발생한다.
    """
    project_root = Path(project_root)
    template = installed_settings_template_path()
    if not template.exists():
        # install 이 0.13.x 이전 버전이면 templates/ 가 깔리지 않았을 수 있다.
        # 상위에서 안내 메시지로 처리.
        return None

    claude_dir = project_root / ".claude"
    target = claude_dir / "settings.local.json"
    claude_dir.mkdir(parents=True, exist_ok=True)

    # idempotent: 이미 올바른 target 을 가리키는 symlink 면 no-op.
    if target.is_symlink():
        try:
            current = os.readlink(target)
        except OSError as exc:
            raise SettingsLinkError(
                f"failed to read existing symlink {target}: {exc}"
            ) from exc
        if Path(current) == template or (claude_dir / current).resolve() == template.resolve():
            return target
        # okstra 가 관리하지 않는 다른 symlink 였으면 backup 후 교체.
        _backup_and_replace(target, template)
        return target

    if target.exists():
        # 일반 파일이 있으면 사용자 작성물일 가능성이 높다 — 손실 방지 backup.
        _backup_and_replace(target, template)
        return target

    try:
        target.symlink_to(template)
    except OSError as exc:
        raise SettingsLinkError(
            f"failed to create symlink {target} -> {template}: {exc}"
        ) from exc
    return target


def _backup_and_replace(target: Path, template: Path) -> None:
    """기존 파일/심볼릭링크를 timestamped backup 으로 옮기고 새 symlink 생성."""
    stamp = time.strftime("%Y%m%d-%H%M%S")
    backup = target.with_name(f"{target.name}.bak.{stamp}")
    try:
        target.rename(backup)
    except OSError as exc:
        raise SettingsLinkError(
            f"failed to back up existing {target} to {backup}: {exc}"
        ) from exc
    try:
        target.symlink_to(template)
    except OSError as exc:
        raise SettingsLinkError(
            f"failed to create symlink {target} -> {template} after backup: {exc}"
        ) from exc
