"""bot author 정규화/검증 헬퍼.

회장 명시 (task-2481): PR author는 정확히 'jeon-jonghyuk-taskctl-bot[bot]' 1개만 허용.
"[bot]" suffix만으로 통과하지 않음 — canonical slug가 BOT_AUTHOR_ALLOWLIST에 있어야 함 (fail-closed).
"""
from __future__ import annotations

import json
import os
from pathlib import Path

BOT_AUTHOR_ALLOWLIST: tuple[str, ...] = ("jeon-jonghyuk-taskctl-bot",)
_WORKSPACE = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
_ALLOWED_BOT_FILE = _WORKSPACE / "memory" / "specs" / "allowed_bot_accounts.json"
_ALLOWED_APPROVERS_FILE = _WORKSPACE / "memory" / "specs" / "allowed_approvers.json"


class BotAuthorError(Exception):
    """bot/human author 검증 실패 시 raise."""


def load_allowed_bot_exact() -> tuple[str, ...]:
    """allowed_bot_accounts.json의 'exact' 목록 로드. 파일 없으면 BOT_AUTHOR_ALLOWLIST fallback."""
    try:
        if _ALLOWED_BOT_FILE.exists():
            data = json.loads(_ALLOWED_BOT_FILE.read_text(encoding="utf-8"))
            exact = data.get("exact") or []
            if exact:
                return tuple(s.strip().lower() for s in exact)
    except Exception:
        pass
    # fallback: BOT_AUTHOR_ALLOWLIST를 [bot] suffix 형태로 변환
    return tuple(f"{slug}[bot]" for slug in BOT_AUTHOR_ALLOWLIST)


def load_allowed_human_approvers() -> tuple[str, ...]:
    """allowed_approvers.json의 'manual_logins' 목록 로드."""
    try:
        if _ALLOWED_APPROVERS_FILE.exists():
            data = json.loads(_ALLOWED_APPROVERS_FILE.read_text(encoding="utf-8"))
            manual = data.get("manual_logins") or []
            return tuple(s.strip() for s in manual if s)
    except Exception:
        pass
    return ()


def normalize_actor(login: str) -> str:
    """gh CLI('app/<slug>') / REST('<slug>[bot]') / 사람 login 모두 canonical slug로 정규화.

    - 'app/foo' → 'foo'
    - 'foo[bot]' → 'foo'
    - 'foo' → 'foo' (소문자)
    """
    if not login:
        return ""
    slug = login.strip().lower()
    if slug.startswith("app/"):
        slug = slug[4:]
    if slug.endswith("[bot]"):
        slug = slug[:-5]
    return slug


def is_bot_author(login: str, *, strict: bool | None = None) -> bool:
    """PR author가 허용된 bot인지 검증.

    strict=True (default for task-2481 commands): canonical slug가 BOT_AUTHOR_ALLOWLIST에 있어야 통과.
        단순 [bot] suffix는 통과 아님 (fail-closed).
    strict=False: 기존 호환 — [bot] suffix / app/ prefix / allowlist canonical 중 하나라도 매칭.

    strict 미지정 시 환경변수 TASKCTL_BOT_AUTHOR_STRICT=1로 활성화 (default 비활성, backwards-compat).
    """
    if not login:
        return False
    if strict is None:
        strict = os.environ.get("TASKCTL_BOT_AUTHOR_STRICT") == "1"
    slug = normalize_actor(login)
    if strict:
        return slug in BOT_AUTHOR_ALLOWLIST
    raw = login.strip().lower()
    if raw.endswith("[bot]") or raw.startswith("app/"):
        return True
    return slug in BOT_AUTHOR_ALLOWLIST


def assert_bot_author(login: str, *, strict: bool | None = None) -> None:
    """bot이 아니면 BotAuthorError raise."""
    if not is_bot_author(login, strict=strict):
        raise BotAuthorError(
            f"PR author '{login}'은 허용된 bot이 아닙니다. "
            f"allowlist={BOT_AUTHOR_ALLOWLIST}"
        )


def assert_human_actor(login: str) -> None:
    """bot이면 BotAuthorError raise (사람 actor 강제)."""
    # strict=False — 모든 [bot]/app/ 패턴은 사람 아님으로 간주
    if is_bot_author(login, strict=False):
        raise BotAuthorError(
            f"reviewer/actor '{login}'은 bot입니다. 사람 login이 필요합니다."
        )


def assert_allowed_human_approver(login: str, *, allow_missing_allowlist: bool = False) -> None:
    """reviewer가 allowed_approvers.json의 manual_logins에 있는지 검증.

    회장 명시: 회장 또는 아누 사람 계정만 approver 허용 (fail-closed).

    allow_missing_allowlist=False (default): allowlist 파일이 없거나 빈 경우 즉시 fail.
    allow_missing_allowlist=True: 환경변수 TASKCTL_ALLOW_MISSING_APPROVER_ALLOWLIST=1 일 때만 사용.
    """
    allowed = load_allowed_human_approvers()
    if not allowed:
        if allow_missing_allowlist or os.environ.get(
            "TASKCTL_ALLOW_MISSING_APPROVER_ALLOWLIST"
        ) == "1":
            return
        raise BotAuthorError(
            f"allowed_approvers.json의 manual_logins가 비어 있거나 파일이 없습니다 "
            f"({_ALLOWED_APPROVERS_FILE}) — fail-closed."
        )
    if login not in allowed:
        raise BotAuthorError(
            f"reviewer '{login}'은 allowed_approvers.json의 manual_logins({list(allowed)})에 없습니다."
        )


def assert_distinct_actors(author: str, reviewer: str) -> None:
    """정규화된 author == reviewer면 BotAuthorError raise (self-approval)."""
    norm_author = normalize_actor(author)
    norm_reviewer = normalize_actor(reviewer)
    if norm_author == norm_reviewer:
        raise BotAuthorError(
            f"self-approval 차단: author '{author}' == reviewer '{reviewer}' "
            f"(canonical: '{norm_author}')"
        )
