"""Claude session helpers.

bash session.sh 의 python 구현. lead claude 세션 관측, resume command
파일 작성. 세션 id 발급기는 워커 dispatch 도 쓰므로
`dispatch_state.generate_claude_session_id` 에 있다.
"""
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Collection

from .dispatch_state import (
    DispatchError,
    load_json_object,
    mutate_team_state,
    worker_session_ids,
)


def _claude_projects_dir_for(cwd: Path) -> Path:
    """Mirror of `okstra_token_usage.paths.claude_project_dir` — kept local to
    avoid a cross-package import inside the run path.
    """
    encoded = "-" + str(cwd).strip("/").replace("/", "-")
    return Path.home() / ".claude" / "projects" / encoded


def resolve_inproc_lead_session_id(project_root: Path) -> str:
    """Best-effort detection of the running Claude session's id when okstra is
    invoked render-only from inside a live session (the `okstra-run` skill
    path). The current session's jsonl is being actively written to under
    `~/.claude/projects/<encoded-cwd>/`, so the most recently modified file
    in that directory is, with very high probability, the calling session.

    Returns the UUID stem on success, empty string on failure (directory
    missing, no jsonl files, permission error). Callers must treat this as
    best-effort — a failure does not invalidate the session, it just means
    auto-detection could not confirm one.
    """
    try:
        d = _claude_projects_dir_for(project_root)
        if not d.exists():
            return ""
        candidates = [p for p in d.iterdir() if p.suffix == ".jsonl"]
        if not candidates:
            return ""
        newest = max(candidates, key=lambda p: p.stat().st_mtime)
        return newest.stem
    except OSError:
        return ""


def _session_has_agent_name(jsonl_path: Path) -> bool:
    """세션 jsonl 에 agentName 이 있으면 worker 세션(lead 아님)."""
    try:
        with jsonl_path.open(encoding="utf-8") as fh:
            for raw in fh:
                try:
                    rec = json.loads(raw)
                except (json.JSONDecodeError, UnicodeDecodeError):
                    continue
                if rec.get("agentName"):
                    return True
    except OSError:
        return False
    return False


def _session_mentions(jsonl_path: Path, needle: str) -> bool:
    """세션 transcript 어딘가에 *needle* 문자열이 등장하는가."""
    try:
        with jsonl_path.open(encoding="utf-8", errors="ignore") as fh:
            return any(needle in line for line in fh)
    except OSError:
        return False


def resolve_lead_session_id_for_run(
    project_root: Path,
    run_dir: Path,
    exclude_session_ids: Collection[str] = (),
) -> str:
    """`run_dir` 를 다룬 lead 세션 중 가장 최근 수정된 것의 id, 없으면 ''.

    프로젝트 디렉토리의 최신 jsonl 을 그대로 집으면(``resolve_inproc_lead_session_id``)
    같은 프로젝트에서 동시에 도는 **다른 태스크의 세션**이 잡힌다 — 실측
    (dev-10172): 남의 세션이 `leadSessionIds` 에 들어가 run 비용이 $80 → $207 로
    부풀고, 그 세션의 PROGRESS 라인이 conformance 스캔에 섞여 오탐을 냈다.
    자기 run 의 산출물 경로를 언급한 세션만 후보로 인정해 그 오염을 막는다.

    `exclude_session_ids` 는 같은 run **안**의 오염을 막는다. cmux 워커는
    `agentName` 을 안 남겨 아래 필터를 통과하고, 워커 세션도 자기 run 의 산출물
    경로를 언급하므로 needle 에도 걸린다. 워커가 리드보다 늦게 끝나면 mtime
    정렬에서 먼저 잡혀 워커 세션이 리드로 기록된다 — dispatch 가 발급한 id 를
    호출자가 넘겨 그 세션들을 후보에서 뺀다.
    """
    proj_dir = _claude_projects_dir_for(project_root)
    try:
        candidates = [p for p in proj_dir.iterdir() if p.suffix == ".jsonl"]
    except OSError:
        return ""
    needle = str(run_dir)
    excluded = set(exclude_session_ids)
    for path in sorted(candidates, key=lambda p: p.stat().st_mtime, reverse=True):
        if path.stem in excluded:
            continue
        if _session_has_agent_name(path):
            continue
        if _session_mentions(path, needle):
            return path.stem
    return ""


def record_observed_lead_session(project_root: Path, team_state_path: Path) -> str:
    """이 run 의 live lead 세션을 관측해 team-state 에 append(멱등). 이 run 을
    다룬 lead 세션을 못 찾거나 중복이면 '' 반환. 재발급으로 갈린 세대를 축1 이
    여기에 모은다.

    후보에서 뺄 워커 세션 id 는 append 대상인 team-state 자체가 들고 있다 —
    dispatch 가 `workerDispatches[].sessionId` 에 적어 둔 값이라, 관측 시점에
    이 run 이 연 워커 세션의 목록은 그 파일 하나로 완결된다.
    """
    try:
        team_state = load_json_object(team_state_path, "team-state")
    except (DispatchError, OSError):
        team_state = {}
    sid = resolve_lead_session_id_for_run(
        project_root,
        team_state_path.parent.parent,
        exclude_session_ids=worker_session_ids(team_state),
    )
    if not sid:
        return ""
    def add_observed_session(state: dict) -> bool:
        lead_ids = state.setdefault("leadSessionIds", [])
        observed = state.setdefault("observedTeamNames", [])
        # 두 키가 list 가 아니면 `sid in None` 이 TypeError 를, `str.append` 가
        # AttributeError 를 낸다. 그 예외는 아래 `(DispatchError, OSError)` 도
        # `observe_lead_session` 의 `OSError` 도 통과해 dispatch 를 죽인다 —
        # 관측이 호출자를 깨뜨리지 않는다는 계약을 docstring 이 아니라 여기서
        # 지킨다. 관측을 건너뛰면 이 run 은 관측 이전과 같은 상태로 남는다.
        if not isinstance(lead_ids, list) or not isinstance(observed, list):
            return False
        if sid in lead_ids:
            return False
        lead_ids.append(sid)
        team = f"session-{sid[:8]}"
        if team not in observed:
            observed.append(team)
        return True

    try:
        changed = mutate_team_state(team_state_path, add_observed_session)
    except (DispatchError, OSError):
        return ""
    return sid if changed else ""


def observe_lead_session(project_root: Path, team_state_path: Path) -> None:
    """단계 경계에서 부르는 부수효과 — 관측 실패를 호출자에게 내지 않는다.

    리드 세션은 resume·compaction 으로 세대가 갈리므로 prepare 때 적힌 단 하나의
    id 는 런 구간에 레코드가 없는 죽은 세션을 가리킬 수 있다. 리드가 반드시
    지나가는 지점마다 관측해 `leadSessionIds` 에 세대를 모은다 — 리드의 협조를
    요구하지 않는 것이 요점이다.

    잡는 범위가 OSError 뿐인 이유: 이 아래 호출들이 각자 자기 실패를 흡수하고,
    그러고도 밖으로 나오는 것이 후보 정렬의 `p.stat()` 이다 — 스캔 도중 세션
    jsonl 이 사라지면 여기서 OSError 가 난다. team-state 읽기·쓰기
    (`load_json_object` / `mutate_team_state`)와 세션 본문 스캔이 그 흡수
    지점이고, 비정상 team-state 가 낼 TypeError/AttributeError 는 잡는 대신 아예
    내지 않는 쪽으로 막는다 — append 대상 키(`leadSessionIds` /
    `observedTeamNames`)는 `add_observed_session` 이, 제외 집합의 출처
    (`workerDispatches`)는 `worker_session_ids` 가 각각 비-list 를 걸러낸다.
    더 넓게 잡으면 team-state 를 깨뜨리는 진짜 버그까지 조용히 삼킨다.
    """
    try:
        record_observed_lead_session(project_root, team_state_path)
    except OSError:
        return


def write_claude_resume_command_file(
    *,
    resume_command_path: Path,
    project_root: Path,
    claude_session_id: str,
    task_key: str,
    task_type: str,
    phase_state: str,
    worker_prompts_dir_relative: str,
    prompt_seq: str,
) -> None:
    """`bash claude-resume-*.sh` 를 실행하면 task 의 claude 세션을 resume
    하도록 shell 스크립트를 작성하고 chmod +x.

    `claude --resume` 자체는 직전 세션 context 만 복구하고 lead 는
    사용자 입력 대기 상태로 멈춘다. 따라서 사용자가 resume 후 무엇을
    입력해야 하는지를 안내하는 guidance 블록을 sh 안에 inline 한다 —
    sh 가 실행될 때 worker prompt 디스크 존재 여부를 직접 검사해
    Phase 2 부터 / Phase 3 부터 중 알맞은 다음 명령을 추천한다.
    """
    resume_command_path = Path(resume_command_path)
    resume_command_path.parent.mkdir(parents=True, exist_ok=True)
    body = f"""#!/usr/bin/env bash
# Generated by okstra. Resume the prepared Claude session for this run.

TASK_KEY={_sh_single_quote(task_key)}
TASK_TYPE={_sh_single_quote(task_type)}
PHASE_STATE={_sh_single_quote(phase_state)}
PROJECT_ROOT={_sh_single_quote(str(project_root))}
WORKER_PROMPTS_DIR="$PROJECT_ROOT/{worker_prompts_dir_relative}"
PROMPT_SEQ={_sh_single_quote(prompt_seq)}
SESSION_ID={_sh_single_quote(claude_session_id)}

cat >&2 <<EOF
============================================================
okstra resume — $TASK_KEY
Phase: $TASK_TYPE ($PHASE_STATE)
============================================================

이 스크립트는 Claude 세션 context 만 복구합니다.
Lead 는 resume 직후 자동으로 진행하지 않으니, 첫 메시지로 다음을
그대로 (또는 상황에 맞게 수정해서) 입력하세요:

EOF

if compgen -G "$WORKER_PROMPTS_DIR/*-worker-prompt-$TASK_TYPE-$PROMPT_SEQ.md" > /dev/null 2>&1; then
  cat >&2 <<EOF
  Phase 3 부터 진행 — implicit team 으로 Phase 4 worker dispatch 까지.
  Worker prompts (이미 작성·저장됨):
    $WORKER_PROMPTS_DIR/*-worker-prompt-$TASK_TYPE-$PROMPT_SEQ.md
EOF
else
  cat >&2 <<EOF
  Phase 2 부터 진행 — worker prompts 작성·저장 후 Phase 3 진행.
  Prompts directory (현재 비어 있음):
    $WORKER_PROMPTS_DIR
EOF
fi

cat >&2 <<EOF

============================================================
EOF

cd "$PROJECT_ROOT"
exec "$HOME/.okstra/bin/okstra.sh" --resume-session "$SESSION_ID" --lead-runtime claude-code --project-root "$PROJECT_ROOT"
"""
    resume_command_path.write_text(body, encoding="utf-8")
    os.chmod(resume_command_path, 0o755)


def _sh_single_quote(value: str) -> str:
    """POSIX-safe single-quote: `it's` → `'it'"'"'s'`."""
    return "'" + value.replace("'", "'\"'\"'") + "'"
