# -*- coding: utf-8 -*-
"""dispatch.anu_activation_preflight — task-2774+5 activation-intended preflight gate.

★ activation-intended 전용 게이트(G2 fail-closed + G3 canonical backlog 보호).
  **호출되는 경우에만 동작**한다 — driver 의 기본(default) 경로는 이 helper 를 호출하지
  않으므로 현행 동작 100% 보존(회귀 0). 이 PR 은 flag/systemd/canary/real activation 을
  켜지 않는다(ACTIVE=false 유지). activation-intended 모드는 코드로 정의만 하고 기본 OFF.

설계 원칙:
- self-contained: stdlib(os) only. driver 모듈 import 0(순환 의존 회피).
- 외부 부작용 0: canonical p0b_inbox backlog 는 **읽기 나열(listdir)만** 수행하고
  삭제/수정/이동/소비 0(파일 개수·내용 불변).
- fail-closed: activation-intended 인데 안전 전제가 깨지면 조용한 None 이 아니라 STOP.

판정 순서(첫 STOP 에서 즉시 반환):
  G2-a is_activated      예외→ACTIVATION_CHECK_ERROR   / 미활성→NOT_ACTIVATED
  G2-b build_governor    예외→GOVERNOR_BUILD_ERROR     / None→GOVERNOR_NOT_INJECTED
  G2-c governor callable 아님→GOVERNOR_NOT_CALLABLE
  G3-d paths             None/빈목록→CANARY_PATHS_REQUIRED
  G3-e canonical backlog paths 밖 backlog 1건이라도→CANONICAL_BACKLOG_DETECTED
  통과                   ok=True / ACTIVATION_INTENDED_READY
"""
from __future__ import annotations

import os
from typing import Optional

# ── 상수 (driver 와 동일 의미. self-contained 위해 로컬 정의 — driver import 0) ─────
ACTIVATION_FLAG_REL = "memory/state/p0b_driver_enabled"
ACTIVATION_ENABLED = "enabled"
INBOX_DIR_REL = "memory/events/p0b_inbox"
RESULT_GLOB = "task-*.result.json"

# ── STOP reason 상수 (테스트/관찰 안정성) ──────────────────────────────────────
REASON_ACTIVATION_CHECK_ERROR = "ACTIVATION_CHECK_ERROR"
REASON_NOT_ACTIVATED = "NOT_ACTIVATED"
REASON_GOVERNOR_BUILD_ERROR = "GOVERNOR_BUILD_ERROR"
REASON_GOVERNOR_NOT_INJECTED = "GOVERNOR_NOT_INJECTED"
REASON_GOVERNOR_NOT_CALLABLE = "GOVERNOR_NOT_CALLABLE"
REASON_CANARY_PATHS_REQUIRED = "CANARY_PATHS_REQUIRED"
REASON_CANONICAL_BACKLOG_DETECTED = "CANONICAL_BACKLOG_DETECTED"
REASON_INBOX_SCAN_ERROR = "INBOX_SCAN_ERROR"
REASON_CANARY_PATH_INVALID = "CANARY_PATH_INVALID"
REASON_READY = "ACTIVATION_INTENDED_READY"


def _stop(reason: str) -> dict:
    return {"ok": False, "reason": reason, "stop": True}


def _ok(reason: str) -> dict:
    return {"ok": True, "reason": reason, "stop": False}


def _norm_under_root(root_real: str, p: str) -> Optional[str]:
    """root 기준 정규화. 상대경로는 root 기준 join 후 realpath.
    정규화 실패/root 밖 경로 → None(비교 불가 → 호출측 fail-closed STOP)."""
    try:
        if not os.path.isabs(p):
            p = os.path.join(root_real, p)
        np = os.path.realpath(os.path.abspath(p))
    except (OSError, ValueError, TypeError):
        return None
    # root-containment 체크: os.path.commonpath 기반(root="/" 등 엣지서 valid sub-path 오판 0).
    #   startswith(root_real + os.sep) 는 root="/" 에서 "//" 비교가 되어 valid path 를 오판함.
    try:
        if os.path.commonpath([root_real, np]) == root_real:
            return np
    except ValueError:  # 다른 드라이브/비교 불가 등
        pass
    return None


def _default_is_activated(root: str) -> bool:
    """activation flag 파일(memory/state/p0b_driver_enabled) 첫 줄 trim == "enabled" 일 때만 True.
    부재/읽기실패 → False(fail-closed). is_activated_fn 미주입 시 self-contained 기본 reader."""
    flag_path = os.path.join(root, ACTIVATION_FLAG_REL)
    try:
        with open(flag_path, "r", encoding="utf-8-sig") as fh:  # utf-8-sig: BOM strip 으로 'enabled' 비교 안정화
            return fh.readline(100).strip() == ACTIVATION_ENABLED
    except (OSError, ValueError):
        return False


def activation_intended_preflight(
    root: str,
    *,
    paths: Optional[list],
    build_governor_fn,
    is_activated_fn=None,
    inbox_dir_rel: Optional[str] = None,
) -> dict:
    """activation-intended 전용 preflight. Decision = dict(ok, reason, stop).

    Args:
        root: canonical root 경로.
        paths: explicit canary scope (처리 대상 result 경로 목록). None/빈목록 → STOP.
        build_governor_fn: callable(root) -> governor_fn|None. activation-gated builder.
        is_activated_fn: callable(root) -> bool. 미주입 시 _default_is_activated(파일 읽기).
        inbox_dir_rel: canonical inbox 상대경로(기본 INBOX_DIR_REL). backlog glob 대상.

    Returns:
        dict(ok: bool, reason: str, stop: bool). 첫 STOP 에서 즉시 반환.
    """
    if is_activated_fn is None:
        is_activated_fn = _default_is_activated
    inbox_rel = inbox_dir_rel if inbox_dir_rel else INBOX_DIR_REL

    # ── G2-a: activation 체크 (조용한 None 금지 — 예외/미활성 모두 STOP) ─────────
    try:
        activated = is_activated_fn(root)
    except Exception:  # noqa: BLE001 — activation 체크 예외 → fail-closed STOP
        return _stop(REASON_ACTIVATION_CHECK_ERROR)
    if not activated:
        return _stop(REASON_NOT_ACTIVATED)

    # ── G2-b: governor build (예외→STOP, None→STOP. activation-intended 에서 미주입 금지) ─
    try:
        governor_fn = build_governor_fn(root)
    except Exception:  # noqa: BLE001 — builder 예외 → fail-closed STOP
        return _stop(REASON_GOVERNOR_BUILD_ERROR)
    if governor_fn is None:
        return _stop(REASON_GOVERNOR_NOT_INJECTED)

    # ── G2-c: governor callable 검증 ─────────────────────────────────────────────
    if not callable(governor_fn):
        return _stop(REASON_GOVERNOR_NOT_CALLABLE)

    # ── root 정규화 (MED-3: canary/backlog 경로 비교는 모두 root 기준) ────────────
    try:
        root_real = os.path.realpath(os.path.abspath(root))
    except (OSError, ValueError):
        root_real = os.path.abspath(root)

    # ── G3-d: explicit canary scope 강제 ────────────────────────────────────────
    if not paths:
        return _stop(REASON_CANARY_PATHS_REQUIRED)

    # canary paths 를 root 기준 정규화 — None 1건이라도 → fail-closed STOP
    norm_paths_list = [_norm_under_root(root_real, p) for p in paths]
    if any(np is None for np in norm_paths_list):
        return _stop(REASON_CANARY_PATH_INVALID)
    norm_paths = set(norm_paths_list)

    # ── G3-e: canonical backlog 보호 (읽기 나열(listdir)만 — 삭제/수정/소비 0) ──
    #   backlog 파일 중 paths 에 없는 것이 1건이라도 있으면 STOP(backlog 미소비 보장).
    #   canary path 가 backlog 밖 격리 inbox 면 backlog 감지 즉시 STOP.
    inbox_path = os.path.join(root, inbox_rel)
    try:
        entries = os.listdir(inbox_path)
    except (FileNotFoundError, NotADirectoryError):
        # inbox 미존재 또는 디렉토리 아님 → backlog 없음(정상), 기존 glob 빈 리스트와 동일
        entries = []
    except OSError:
        # 권한 등 나열 불가 → backlog 유무 판정 불가 → fail-closed STOP
        return _stop(REASON_INBOX_SCAN_ERROR)

    backlog = [
        os.path.join(inbox_path, name)
        for name in entries
        if name.startswith("task-") and name.endswith(".result.json")
    ]

    for backlog_file in backlog:
        norm_bf = _norm_under_root(root_real, backlog_file)
        if norm_bf is None or norm_bf not in norm_paths:
            return _stop(REASON_CANONICAL_BACKLOG_DETECTED)

    # ── 전부 통과 ────────────────────────────────────────────────────────────────
    return _ok(REASON_READY)


__all__ = [
    "activation_intended_preflight",
    "ACTIVATION_FLAG_REL",
    "ACTIVATION_ENABLED",
    "INBOX_DIR_REL",
    "RESULT_GLOB",
    "REASON_ACTIVATION_CHECK_ERROR",
    "REASON_NOT_ACTIVATED",
    "REASON_GOVERNOR_BUILD_ERROR",
    "REASON_GOVERNOR_NOT_INJECTED",
    "REASON_GOVERNOR_NOT_CALLABLE",
    "REASON_CANARY_PATHS_REQUIRED",
    "REASON_CANONICAL_BACKLOG_DETECTED",
    "REASON_INBOX_SCAN_ERROR",
    "REASON_CANARY_PATH_INVALID",
    "REASON_READY",
]
