"""anu_v2.owner_trigger_entry — OwnerTriggerOnly composition root + CLI/scheduler 진입점.

task-2699 production wiring: http_post/token_provider 주입 + 단발 CLI + 자동 scheduler.

one-way isolation:
  본 모듈은 anu_v2 내부 + stdlib(subprocess, argparse, json, os, sys) 만 import.
  composition root 이므로 subprocess/argparse 허용. 외부 utils/dispatch 의존성 0.

보안 원칙:
  - token raw 값을 stdout/stderr/예외메시지 어디에도 출력 금지.
  - COMMENT_BODY 외 body 인자 일체 없음 (코어가 강제, entry 는 외부 body 인자 미수령).
  - merge/push/PR/admin endpoint 추가 0.
  - BOT_GITHUB_TOKEN 등 다른 token fallback 0.
  - subprocess 실행 시 token 이 환경변수로 전달되지 않도록 env 주입 방식 사용.
"""

from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Callable, Mapping, Sequence

from anu_v2.executor_scheduler import ExecutorScheduler
from anu_v2.idle_pr_diagnoser import IdlePRSnapshot, GeminiReviewMeta
from anu_v2.merge_queue_executor import (
    AuditWriter,
    GhRunner,
    GitRunner,
    MergeQueueExecutor,
    PytestRunner,
)
from anu_v2.owner_trigger_http_post import (
    make_owner_trigger_token_provider,
    make_production_http_post,
)
from anu_v2.owner_trigger_only import (
    OwnerTriggerOnly,
    TOKEN_ENV_NAME,
    invoke_from_scheduler,
)


# ─── F-4: gh/git subprocess 에서 제거할 민감 환경변수 목록 ──────────────────────

_SENSITIVE_ENV_NAMES: tuple[str, ...] = (TOKEN_ENV_NAME,)


# ─── production runner 팩토리 (composition root, subprocess 허용) ─────────────


def _make_gh_runner() -> GhRunner:
    """production gh CLI runner (subprocess 기반).

    F-4 보안 하드닝: OWNER_GEMINI_TRIGGER_TOKEN 을 gh subprocess 환경에서
    항상 제거. gh runner 는 BOT_GITHUB_TOKEN 만 필요하며 OWNER 토큰은 불필요.
    """

    def _gh(args: Sequence[str], env: Mapping[str, str] | None) -> subprocess.CompletedProcess:
        cmd = ["gh", *args]
        # F-4: os.environ 복사 후 OWNER 토큰을 항상 제거
        base = dict(os.environ)
        for k in _SENSITIVE_ENV_NAMES:
            base.pop(k, None)
        if env is not None:
            # env 로 명시 주입된 것도 sensitive 면 제거 (gh 는 OWNER 토큰 불필요)
            for k, v in env.items():
                if k not in _SENSITIVE_ENV_NAMES:
                    base[k] = v
        run_env = base
        return subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            env=run_env,
        )

    return _gh


def _make_git_runner() -> GitRunner:
    """production git CLI runner (subprocess 기반).

    F-4 보안 하드닝: OWNER_GEMINI_TRIGGER_TOKEN 을 git subprocess 환경에서
    항상 제거.
    """

    def _git(args: Sequence[str]) -> subprocess.CompletedProcess:
        cmd = ["git", *args]
        # F-4: os.environ 복사 후 OWNER 토큰을 항상 제거
        run_env = dict(os.environ)
        for k in _SENSITIVE_ENV_NAMES:
            run_env.pop(k, None)
        return subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            env=run_env,
        )

    return _git


def _make_pytest_runner() -> PytestRunner:
    """production pytest runner (subprocess 기반). 종료코드 반환."""

    def _pytest(args: Sequence[str]) -> int:
        cmd = [sys.executable, "-m", "pytest", *args]
        result = subprocess.run(
            cmd,
            capture_output=False,
        )
        return result.returncode

    return _pytest


def _make_audit_writer(path: str | Path) -> AuditWriter:
    """JSONL append audit_writer 팩토리."""
    audit_path = Path(path)

    def _writer(record: Mapping) -> None:
        audit_path.parent.mkdir(parents=True, exist_ok=True)
        with open(audit_path, "a", encoding="utf-8") as fh:
            fh.write(json.dumps(dict(record), ensure_ascii=False, sort_keys=True) + "\n")

    return _writer


# ─── production snapshot_provider (gh pr list 기반) ──────────────────────────


def _gh_snapshot_provider(owner: str, repo: str) -> list[IdlePRSnapshot]:
    """gh pr list --state open --json 으로 IdlePRSnapshot 리스트 반환.

    dry_run / test 에서는 snapshot_provider 주입으로 대체 가능.
    """
    fields = "number,headRefOid,headRefName,createdAt,state,author,reviews"
    cmd = [
        "gh", "pr", "list",
        "--repo", f"{owner}/{repo}",
        "--state", "open",
        "--json", fields,
        "--limit", "100",
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        # gh 실패 시 빈 리스트 반환 (fail-safe)
        return []

    try:
        prs = json.loads(result.stdout)
    except Exception:
        return []

    snapshots: list[IdlePRSnapshot] = []
    for pr in prs:
        try:
            # Gemini review 메타 파싱 (reviews 필드가 있으면)
            gemini_reviews: list[GeminiReviewMeta] = []
            for review in pr.get("reviews", []):
                body = review.get("body", "")
                commit_id = review.get("commit", {}).get("oid", "")
                submitted = review.get("submittedAt", "")
                if (
                    isinstance(commit_id, str)
                    and len(commit_id) == 40
                    and all(c in "0123456789abcdefABCDEF" for c in commit_id)
                    and isinstance(submitted, str)
                    and submitted
                    and "gemini" in body.lower()
                ):
                    gemini_reviews.append(
                        GeminiReviewMeta(
                            commit_id=commit_id.lower(),
                            submitted_at=submitted,
                        )
                    )

            head_sha = pr.get("headRefOid", "")
            if len(head_sha) != 40:
                continue

            author = pr.get("author", {})
            author_login = author.get("login", "") if isinstance(author, dict) else ""
            author_is_bot = "[bot]" in author_login or author.get("is_bot", False)

            snapshot = IdlePRSnapshot(
                number=int(pr["number"]),
                head_sha=head_sha.lower(),
                head_ref=pr.get("headRefName", ""),
                created_at=pr.get("createdAt", ""),
                gemini_reviews=tuple(gemini_reviews),
                ci_required_all_success=False,  # gh pr list에서 CI 상태 직접 미제공
                state=pr.get("state", "OPEN"),
                author_is_bot=bool(author_is_bot),
            )
            snapshots.append(snapshot)
        except Exception:
            continue

    return snapshots


# ─── build_runner / run_single ────────────────────────────────────────────────


def build_runner(
    *,
    workspace_root: str | Path,
    dry_run: bool = False,
    env: Mapping[str, str] | None = None,
    http_post: Callable[[str, str, dict, dict], dict] | None = None,
    token_provider: Callable[[], str] | None = None,
) -> OwnerTriggerOnly:
    """OwnerTriggerOnly 인스턴스 생성 + http_post/token_provider 주입.

    http_post 미주입 시 make_production_http_post(dry_run=dry_run),
    token_provider 미주입 시 make_owner_trigger_token_provider(env).

    Args:
        workspace_root: anu_v2 workspace 루트.
        dry_run: True 면 네트워크 호출 0 (make_production_http_post dry_run 전달).
        env: token 환경변수 dict. None 이면 os.environ 에서 읽음.
        http_post: 테스트 주입용. None 이면 production http_post 사용.
        token_provider: 테스트 주입용. None 이면 production token_provider 사용.

    Returns:
        OwnerTriggerOnly 인스턴스.
    """
    effective_http_post = http_post if http_post is not None else make_production_http_post(
        dry_run=dry_run
    )
    effective_token_provider = (
        token_provider
        if token_provider is not None
        else make_owner_trigger_token_provider(env)
    )
    return OwnerTriggerOnly(
        workspace_root=workspace_root,
        http_post=effective_http_post,
        token_provider=effective_token_provider,
        audit=None,
    )


def run_single(
    *,
    workspace_root: str | Path,
    decision_path: str | Path,
    owner: str,
    repo: str,
    current_head_actual: str,
    dry_run: bool = False,
    env: Mapping[str, str] | None = None,
    http_post: Callable[[str, str, dict, dict], dict] | None = None,
    token_provider: Callable[[], str] | None = None,
) -> str:
    """단발 CLI 경로: build_runner → invoke_from_scheduler → 결과 status 반환.

    Args:
        workspace_root: anu_v2 workspace 루트.
        decision_path: owner_trigger_decision.json 경로.
        owner: GitHub owner.
        repo: GitHub repo.
        current_head_actual: 실측 PR head SHA (40-char hex).
        dry_run: True 면 네트워크 호출 0.
        env: token 환경변수 dict.
        http_post: 테스트 주입용.
        token_provider: 테스트 주입용.

    Returns:
        "POSTED" | "DEDUPED" | "FAILED" | "PENDING"
    """
    runner = build_runner(
        workspace_root=workspace_root,
        dry_run=dry_run,
        env=env,
        http_post=http_post,
        token_provider=token_provider,
    )
    return invoke_from_scheduler(
        runner,
        decision_path=decision_path,
        owner=owner,
        repo=repo,
        current_head_actual=current_head_actual,
    )


# ─── build_scheduler / run_scheduler_cycle ────────────────────────────────────


def build_scheduler(
    *,
    workspace_root: str | Path,
    decision_dir: str | Path,
    owner: str,
    repo: str,
    snapshot_provider: Callable[[], Sequence[IdlePRSnapshot]],
    dry_run: bool = False,
    env: Mapping[str, str] | None = None,
    http_post: Callable[[str, str, dict, dict], dict] | None = None,
    token_provider: Callable[[], str] | None = None,
    merge_executor: MergeQueueExecutor | None = None,
) -> ExecutorScheduler:
    """ExecutorScheduler 인스턴스 생성.

    runner = build_runner(...). merge_executor 미주입 시 production runner 들로
    MergeQueueExecutor 생성.

    Args:
        workspace_root: anu_v2 workspace 루트.
        decision_dir: marker / decision.json 디렉토리.
        owner: GitHub owner.
        repo: GitHub repo.
        snapshot_provider: Callable[[], Sequence[IdlePRSnapshot]].
        dry_run: True 면 네트워크 호출 0.
        env: token 환경변수 dict.
        http_post: 테스트 주입용.
        token_provider: 테스트 주입용.
        merge_executor: 테스트 주입용. None 이면 production MergeQueueExecutor.

    Returns:
        ExecutorScheduler 인스턴스.
    """
    runner = build_runner(
        workspace_root=workspace_root,
        dry_run=dry_run,
        env=env,
        http_post=http_post,
        token_provider=token_provider,
    )

    if merge_executor is None:
        workspace_path = Path(workspace_root).resolve()
        audit_path = workspace_path / "memory" / "events" / "merge-executor-audit.jsonl"
        merge_executor = MergeQueueExecutor(
            gh_runner=_make_gh_runner(),
            git_runner=_make_git_runner(),
            pytest_runner=_make_pytest_runner(),
            audit_writer=_make_audit_writer(audit_path),
            task_md_root=workspace_path / "memory" / "tasks",
        )

    return ExecutorScheduler(
        workspace_root=workspace_root,
        decision_dir=decision_dir,
        snapshot_provider=snapshot_provider,
        owner_trigger=runner,
        merge_executor=merge_executor,
        owner=owner,
        repo=repo,
    )


def run_scheduler_cycle(
    *,
    workspace_root: str | Path,
    decision_dir: str | Path,
    owner: str,
    repo: str,
    snapshot_provider: Callable[[], Sequence[IdlePRSnapshot]],
    dry_run: bool = False,
    env: Mapping[str, str] | None = None,
    http_post: Callable[[str, str, dict, dict], dict] | None = None,
    token_provider: Callable[[], str] | None = None,
    merge_executor: MergeQueueExecutor | None = None,
):
    """build_scheduler → run_one_cycle 호출.

    Args: build_scheduler 와 동일 + snapshot_provider.

    Returns:
        SchedulerCycleResult.
    """
    scheduler = build_scheduler(
        workspace_root=workspace_root,
        decision_dir=decision_dir,
        owner=owner,
        repo=repo,
        snapshot_provider=snapshot_provider,
        dry_run=dry_run,
        env=env,
        http_post=http_post,
        token_provider=token_provider,
        merge_executor=merge_executor,
    )
    return scheduler.run_one_cycle(env=dict(env) if env is not None else None)


# ─── CLI entry point ──────────────────────────────────────────────────────────


def main(argv: list[str] | None = None) -> int:
    """CLI entry point.

    서브커맨드:
      trigger: 단발 owner trigger 실행.
      scan:    scheduler 1 cycle 실행.

    보안: token/credential 을 stdout/stderr 에 절대 print 금지. status enum 문자열만 출력.
    """
    parser = argparse.ArgumentParser(
        prog="owner_trigger_entry",
        description="OwnerTriggerOnly CLI/scheduler entry point (task-2699)",
    )
    sub = parser.add_subparsers(dest="command", required=True)

    # ── subcommand: trigger (단발 CLI) ──────────────────────────────────────
    trigger_p = sub.add_parser("trigger", help="단발 owner trigger 실행")
    trigger_p.add_argument("--workspace-root", required=True, help="workspace root 경로")
    trigger_p.add_argument("--decision-path", required=True, help="owner_trigger_decision.json 경로")
    trigger_p.add_argument("--owner", required=True, help="GitHub owner")
    trigger_p.add_argument("--repo", required=True, help="GitHub repo")
    trigger_p.add_argument("--current-head", required=True, help="실측 PR head SHA (40-char hex)")
    trigger_p.add_argument("--dry-run", action="store_true", default=False, help="네트워크 호출 0")

    # ── subcommand: scan (scheduler 1 cycle) ────────────────────────────────
    scan_p = sub.add_parser("scan", help="scheduler 1 cycle 실행")
    scan_p.add_argument("--workspace-root", required=True, help="workspace root 경로")
    scan_p.add_argument("--decision-dir", required=True, help="decision/marker 디렉토리")
    scan_p.add_argument("--owner", required=True, help="GitHub owner")
    scan_p.add_argument("--repo", required=True, help="GitHub repo")
    scan_p.add_argument("--dry-run", action="store_true", default=False, help="네트워크 호출 0")

    args = parser.parse_args(argv)

    # env 는 os.environ 에서 직접 읽음 (CLI 에서 직접 token 인자 없음 — 보안 원칙)
    env_map = dict(os.environ)

    if args.command == "trigger":
        try:
            status = run_single(
                workspace_root=args.workspace_root,
                decision_path=args.decision_path,
                owner=args.owner,
                repo=args.repo,
                current_head_actual=args.current_head,
                dry_run=args.dry_run,
                env=env_map,
            )
            # token/credential 미출력 — status enum 문자열만 출력
            print(status)
            return 0
        except Exception as exc:
            # 예외 메시지에 token 이 섞이지 않도록 type 이름만 출력
            print(f"ERROR: {type(exc).__name__}", file=sys.stderr)
            return 1

    elif args.command == "scan":
        # production snapshot_provider — owner/repo 기반 gh pr list
        owner = args.owner
        repo = args.repo

        def _snap_provider() -> list[IdlePRSnapshot]:
            return _gh_snapshot_provider(owner, repo)

        try:
            result = run_scheduler_cycle(
                workspace_root=args.workspace_root,
                decision_dir=args.decision_dir,
                owner=args.owner,
                repo=args.repo,
                snapshot_provider=_snap_provider,
                dry_run=args.dry_run,
                env=env_map,
            )
            # 결과 요약만 출력 (token 미포함)
            print(
                f"cycle_finished={result.cycle_finished_at} "
                f"pr_actions={len(result.pr_actions)} "
                f"bot_should_exit={result.bot_should_exit}"
            )
            return 0
        except Exception as exc:
            print(f"ERROR: {type(exc).__name__}", file=sys.stderr)
            return 1

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
