#!/usr/bin/env python3
"""봇 → 아누 후속 알림 메시지 추출 + 포맷 + 발송 헬퍼 (task-2360).

3단 우선순위로 메시지 항목을 수집한다:
1. memory/events/<task>.followup.txt — 봇이 명시적으로 작성한 강조 메모
2. memory/reports/<task>.md — 보고서에서 정규식으로 자동 추출
3. memory/events/<task>.completion.txt — 폴백 1줄 요약

표준 5섹션 메시지 포맷 (아누 chat에 cron 발송):
- 핵심 결과
- 회장 결정 필요
- 머지 필요
- 미해결
- 다음 단계 권장

CLI 사용법:
    python3 extract_followup.py extract <task_id>           # 항목 dict JSON 출력
    python3 extract_followup.py format <task_id>            # 메시지 텍스트 출력
    python3 extract_followup.py send <task_id> [--dry-run]  # 메시지 cron 발송
"""

from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
EVENTS_DIR = Path(WORKSPACE_ROOT) / "memory" / "events"
REPORTS_DIR = Path(WORKSPACE_ROOT) / "memory" / "reports"
TASKS_DIR = Path(WORKSPACE_ROOT) / "memory" / "tasks"
TIMERS_FILE = Path(WORKSPACE_ROOT) / "memory" / "task-timers.json"
LOG_FILE = Path(WORKSPACE_ROOT) / "logs" / "anu-notify.log"
KST = timezone(timedelta(hours=9))

ANU_CHAT_ID_DEFAULT = "6937032012"
ANU_KEY_DEFAULT = "c119085addb0f8b7"
TELEGRAM_LIMIT = 4000  # 4096보다 보수적
MIN_HEADROOM = 256


def _log(task_id: str, message: str) -> None:
    """logs/anu-notify.log 추가."""
    ts = datetime.now(KST).isoformat()
    line = f"[{ts}] [extract-followup] {task_id}: {message}\n"
    try:
        LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(line)
    except OSError:
        pass


def _load_team_and_bot(task_id: str) -> tuple[str, str]:
    """task-timers.json에서 (team_id, bot_persona) 조회. 실패 시 ('', '')."""
    team_id = ""
    bot_persona = ""
    try:
        if TIMERS_FILE.exists():
            data = json.loads(TIMERS_FILE.read_text(encoding="utf-8"))
            tasks = data.get("tasks", data)
            t = tasks.get(task_id, {}) or {}
            team_id = (t.get("team_id") or t.get("team") or "").strip()
            bot_persona = (t.get("persona") or t.get("bot") or "").strip()
    except (OSError, json.JSONDecodeError):
        pass
    return team_id, bot_persona


def _parse_followup_txt(text: str) -> dict:
    """followup.txt를 마크다운 섹션 단위로 파싱.

    예상 헤더 (대소문자/공백 관용):
    - ## 회장 결정 필요
    - ## 머지 필요
    - ## 미해결
    - ## 다음 단계 권장
    - ## 핵심 결과 (있으면 우선)

    각 섹션의 bullet(`- ` 또는 `* `)을 항목 리스트로 수집한다.
    헤더 외 본문은 무시한다.
    """
    sections: dict[str, list[str]] = {
        "core_result": [],
        "decisions_needed": [],
        "merges_needed": [],
        "unresolved": [],
        "next_steps": [],
    }

    header_map = [
        (re.compile(r"^#+\s*핵심\s*결과", re.IGNORECASE), "core_result"),
        (re.compile(r"^#+\s*회장\s*(결정|승인)", re.IGNORECASE), "decisions_needed"),
        (re.compile(r"^#+\s*머지\s*(필요|판단)?", re.IGNORECASE), "merges_needed"),
        (re.compile(r"^#+\s*(미해결|follow.?up|남은)", re.IGNORECASE), "unresolved"),
        (re.compile(r"^#+\s*(다음\s*단계|next\s*steps?|후속)", re.IGNORECASE), "next_steps"),
    ]

    current: str | None = None
    free_lines: list[str] = []
    for raw in text.splitlines():
        line = raw.rstrip()
        # 헤더 매칭
        if line.lstrip().startswith("#"):
            matched = None
            for pat, key in header_map:
                if pat.match(line.lstrip()):
                    matched = key
                    break
            current = matched
            continue
        if current is None:
            if line.strip():
                free_lines.append(line.strip())
            continue
        # bullet 수집
        m = re.match(r"^\s*[-*]\s+(.+)$", line)
        if m:
            sections[current].append(m.group(1).strip())
        elif line.strip() and current == "core_result":
            # 핵심 결과는 본문 라인 그대로 수집 (불릿 아니어도 됨)
            sections[current].append(line.strip())

    # 헤더가 하나도 없는 followup.txt는 전체를 core_result로 취급
    if not any(sections.values()) and free_lines:
        sections["core_result"] = free_lines

    return sections


def _read_followup_file(task_id: str) -> dict | None:
    """followup.txt가 있으면 dict 반환, 없으면 None."""
    fp = EVENTS_DIR / f"{task_id}.followup.txt"
    if not fp.exists():
        return None
    try:
        text = fp.read_text(encoding="utf-8")
    except OSError:
        return None
    if not text.strip():
        return None
    return _parse_followup_txt(text)


_BULLET_RE = re.compile(r"^\s*(?:[-*]|\d+\.)\s+(.+)$")


def _extract_section_bullets(content: str, header_patterns: list[str]) -> list[str]:
    """보고서에서 헤더 매칭되는 섹션 bullet들을 수집."""
    pat = re.compile("|".join(header_patterns), re.IGNORECASE)
    lines = content.split("\n")
    items: list[str] = []
    in_section = False
    for line in lines:
        # 새 ## 또는 ### 헤더
        if re.match(r"^#{1,4}\s+", line):
            if pat.search(line):
                in_section = True
                continue
            if in_section:
                # 다른 헤더가 ## 이상 상위 레벨이면 섹션 종료. ### 서브헤더는 그대로 둔다.
                level_match = re.match(r"^(#+)", line)
                cur_level = len(level_match.group(1)) if level_match else 99
                if cur_level <= 2:
                    in_section = False
            continue
        if not in_section:
            continue
        m = _BULLET_RE.match(line)
        if m:
            text = m.group(1).strip()
            # 굵은 글씨, 인라인 코드 등 제거 후 trim
            cleaned = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
            cleaned = re.sub(r"`([^`]+)`", r"\1", cleaned)
            if cleaned:
                items.append(cleaned)
    return items


def _extract_core_result(content: str) -> str | None:
    """SCQA의 A 섹션 또는 첫 본문 한 단락 추출."""
    lines = content.split("\n")

    # A 패턴
    a_pat = re.compile(r"^\s*\*\*A[.:]?\*\*[.:]?\s*")
    for i, line in enumerate(lines):
        if a_pat.match(line):
            tail = a_pat.sub("", line).strip()
            collected = [tail] if tail else []
            for nxt in lines[i + 1 :]:
                if nxt.startswith("---"):
                    break
                if re.match(r"^\s*\*\*[A-Z]\*\*", nxt):
                    break
                if nxt.startswith("#"):
                    break
                collected.append(nxt)
            text = "\n".join(collected).strip()
            if text:
                return text

    # ### Answer 헤더
    in_ans = False
    paragraph: list[str] = []
    for line in lines:
        if re.match(r"^#{2,4}\s*Answer\b", line, re.IGNORECASE):
            in_ans = True
            continue
        if in_ans:
            if line.startswith("#"):
                break
            if line.strip():
                paragraph.append(line.strip())
            elif paragraph:
                break
    if paragraph:
        return " ".join(paragraph).strip()

    # 첫 ## 섹션의 첫 단락
    first_section_idx: int | None = None
    for i, line in enumerate(lines):
        if line.startswith("## "):
            first_section_idx = i
            break
    if first_section_idx is not None:
        para: list[str] = []
        in_para = False
        for line in lines[first_section_idx + 1 :]:
            if line.startswith("#"):
                if in_para:
                    break
                continue
            if line.strip():
                in_para = True
                para.append(line.strip())
            elif in_para:
                break
        text = " ".join(para).strip()
        if text:
            return text
    return None


def _read_report(task_id: str) -> dict | None:
    """보고서에서 자동 추출."""
    fp = REPORTS_DIR / f"{task_id}.md"
    if not fp.exists():
        return None
    try:
        content = fp.read_text(encoding="utf-8")
    except OSError:
        return None

    sections = {
        "core_result": [],
        "decisions_needed": [],
        "merges_needed": [],
        "unresolved": [],
        "next_steps": [],
    }
    core = _extract_core_result(content)
    if core:
        sections["core_result"] = [core]

    sections["decisions_needed"] = _extract_section_bullets(
        content, [r"^#+\s*회장\s*(결정|승인|결재)", r"^#+\s*decisions?\s*needed"]
    )
    sections["merges_needed"] = _extract_section_bullets(
        content, [r"^#+\s*머지\s*(필요|판단)", r"^#+\s*merge\s*(needed|판단)"]
    )
    sections["unresolved"] = _extract_section_bullets(
        content,
        [
            r"^#+\s*(범위\s*내\s*)?미해결",
            r"^#+\s*follow.?up",
            r"^#+\s*발견\s*이슈",
        ],
    )
    sections["next_steps"] = _extract_section_bullets(
        content,
        [
            r"^#+\s*다음\s*단계",
            r"^#+\s*후속\s*task",
            r"^#+\s*next\s*steps?",
        ],
    )
    return sections


def _read_completion(task_id: str) -> str | None:
    fp = EVENTS_DIR / f"{task_id}.completion.txt"
    if not fp.exists():
        return None
    try:
        text = fp.read_text(encoding="utf-8").strip()
    except OSError:
        return None
    return text or None


def extract_followup(task_id: str) -> dict:
    """3단 우선순위로 followup 항목 추출.

    Returns:
        {
          "source": "followup_txt" | "report" | "completion" | "none",
          "core_result": str | None,
          "decisions_needed": list[str],
          "merges_needed": list[str],
          "unresolved": list[str],
          "next_steps": list[str],
        }
    """
    out = {
        "source": "none",
        "core_result": None,
        "decisions_needed": [],
        "merges_needed": [],
        "unresolved": [],
        "next_steps": [],
    }

    fu = _read_followup_file(task_id)
    rep = _read_report(task_id)

    # followup.txt 항목 우선, 부족한 섹션은 보고서로 보충
    merged = {
        "core_result": [],
        "decisions_needed": [],
        "merges_needed": [],
        "unresolved": [],
        "next_steps": [],
    }
    used_sources: list[str] = []
    if fu:
        for k in merged:
            if fu.get(k):
                merged[k] = list(fu[k])
        used_sources.append("followup_txt")
    if rep:
        for k in merged:
            if not merged[k] and rep.get(k):
                merged[k] = list(rep[k])
        if any(rep.values()):
            used_sources.append("report")

    has_any = any(v for v in merged.values())
    if not has_any:
        # completion.txt 폴백
        comp = _read_completion(task_id)
        if comp:
            # 첫 줄 또는 첫 단락
            first_para = comp.split("\n\n", 1)[0].strip()
            merged["core_result"] = [first_para]
            used_sources.append("completion")

    if merged["core_result"]:
        out["core_result"] = merged["core_result"][0] if len(merged["core_result"]) == 1 else "\n".join(merged["core_result"])
    out["decisions_needed"] = merged["decisions_needed"]
    out["merges_needed"] = merged["merges_needed"]
    out["unresolved"] = merged["unresolved"]
    out["next_steps"] = merged["next_steps"]
    out["source"] = "+".join(used_sources) if used_sources else "none"
    return out


def _truncate_bullets(items: list[str], budget: int) -> tuple[list[str], int]:
    """문자 budget 안에서 bullet들을 자른다. 반환 (수용된 항목, 잘린 개수)."""
    accepted: list[str] = []
    used = 0
    for item in items:
        line = f"- {item}\n"
        if used + len(line) > budget and accepted:
            return accepted, len(items) - len(accepted)
        accepted.append(item)
        used += len(line)
    return accepted, 0


def format_anu_message(
    task_id: str,
    team: str,
    bot: str,
    items: dict,
) -> str:
    """표준 5섹션 메시지 생성.

    Telegram 4096자 제한 → 4000자 안전 마진. 초과 시 보고서 경로만 안내.
    """
    title_parts = [f"★ {task_id} 작업 완료 보고"]
    suffix_pieces: list[str] = []
    if bot:
        suffix_pieces.append(bot)
    if team:
        suffix_pieces.append(team)
    if suffix_pieces:
        title_parts.append(f"({', '.join(suffix_pieces)})")
    header = " ".join(title_parts)

    report_rel = f"memory/reports/{task_id}.md"
    footer = f"\n\n상세 보고서: {report_rel}"

    sections: list[tuple[str, list[str] | str]] = [
        ("핵심 결과", items.get("core_result") or "—"),
        ("★ 회장 결정 필요", items.get("decisions_needed") or []),
        ("★ 머지 필요", items.get("merges_needed") or []),
        ("★ 미해결", items.get("unresolved") or []),
        ("★ 다음 단계 권장", items.get("next_steps") or []),
    ]

    body_lines: list[str] = []
    for label, val in sections:
        body_lines.append("")
        body_lines.append(label + ":")
        if isinstance(val, str):
            body_lines.append(val)
        elif val:
            for v in val:
                body_lines.append(f"- {v}")
        else:
            body_lines.append("- 없음")

    full = header + "\n" + "\n".join(body_lines).lstrip("\n") + footer
    if len(full) <= TELEGRAM_LIMIT:
        return full

    # 초과 시: 회장결정 + 머지 + 미해결만 우선 보존, 나머지는 잘라낸다
    budget = TELEGRAM_LIMIT - len(header) - len(footer) - MIN_HEADROOM
    if budget < 0:
        return f"{header}{footer}".strip()

    rebuilt: list[str] = []
    priority_order = [
        ("핵심 결과", items.get("core_result") or "—"),
        ("★ 회장 결정 필요", items.get("decisions_needed") or []),
        ("★ 머지 필요", items.get("merges_needed") or []),
        ("★ 미해결", items.get("unresolved") or []),
        ("★ 다음 단계 권장", items.get("next_steps") or []),
    ]
    used = 0
    for label, val in priority_order:
        seg_header = f"\n\n{label}:\n"
        if isinstance(val, str):
            text = val if used + len(seg_header) + len(val) <= budget else val[: max(0, budget - used - len(seg_header))]
            seg = seg_header + text
        elif val:
            kept, dropped = _truncate_bullets(val, max(0, budget - used - len(seg_header)))
            seg = seg_header + "\n".join(f"- {x}" for x in kept)
            if dropped:
                seg += f"\n- (외 {dropped}건은 보고서 참조)"
        else:
            seg = seg_header + "- 없음"
        if used + len(seg) > budget:
            rebuilt.append(seg_header + "(생략 — 보고서 참조)")
            break
        rebuilt.append(seg)
        used += len(seg)

    return header + "".join(rebuilt) + footer


def _at_time_seconds(seconds: int) -> str:
    """현재 시각 + N초를 cokacdir --at 형식으로.

    cokacdir는 ISO T-구분자를 거부한다. 'YYYY-MM-DD HH:MM:SS' (공백 구분)이 표준.
    """
    return (datetime.now(KST) + timedelta(seconds=seconds)).strftime("%Y-%m-%d %H:%M:%S")


def send_anu_cron(
    task_id: str,
    message: str,
    chat_id: str | None = None,
    anu_key: str | None = None,
    delay_seconds: int = 10,
    dry_run: bool = False,
) -> tuple[bool, str]:
    """cokacdir --cron로 아누 chat에 메시지 발송 + .anu-notified 마커 생성.

    중복 방지: .anu-notified 이미 존재 시 발송 스킵.
    """
    notified_marker = EVENTS_DIR / f"{task_id}.anu-notified"
    if notified_marker.exists():
        return False, "already-notified"

    chat_id = chat_id or os.environ.get("COKACDIR_CHAT_ID") or ANU_CHAT_ID_DEFAULT
    anu_key = anu_key or os.environ.get("COKACDIR_KEY_ANU") or ANU_KEY_DEFAULT

    cokacdir_bin = shutil.which("cokacdir") or "/usr/local/bin/cokacdir"
    cmd = [
        cokacdir_bin,
        "--cron",
        message,
        "--at",
        _at_time_seconds(delay_seconds),
        "--chat",
        chat_id,
        "--key",
        anu_key,
        "--once",
    ]

    if dry_run:
        return True, "dry-run: " + " ".join(cmd[:3]) + " ..."

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    except (subprocess.TimeoutExpired, OSError) as e:
        _log(task_id, f"cokacdir 실행 예외: {e}")
        return False, f"exception: {e}"

    if result.returncode != 0:
        _log(task_id, f"cokacdir 실패 rc={result.returncode}: {result.stderr.strip()[:200]}")
        return False, f"rc={result.returncode}: {result.stderr.strip()[:200]}"

    # 마커 원자적 생성
    try:
        EVENTS_DIR.mkdir(parents=True, exist_ok=True)
        fd = os.open(str(notified_marker), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
        try:
            os.write(fd, datetime.now(KST).isoformat().encode())
        finally:
            os.close(fd)
    except FileExistsError:
        pass
    except OSError as e:
        _log(task_id, f"마커 생성 실패: {e}")

    _log(task_id, f"아누 cron 발송 완료 (delay={delay_seconds}s)")
    return True, "sent"


def build_message_for_task(task_id: str) -> tuple[str, dict]:
    items = extract_followup(task_id)
    team_id, bot = _load_team_and_bot(task_id)
    msg = format_anu_message(task_id, team_id, bot, items)
    return msg, {"items": items, "team_id": team_id, "bot": bot}


def _cmd_extract(task_id: str) -> int:
    items = extract_followup(task_id)
    print(json.dumps(items, ensure_ascii=False, indent=2))
    return 0


def _cmd_format(task_id: str) -> int:
    msg, _ = build_message_for_task(task_id)
    print(msg)
    return 0


def _cmd_send(task_id: str, dry_run: bool, delay: int) -> int:
    msg, meta = build_message_for_task(task_id)
    ok, status = send_anu_cron(task_id, msg, delay_seconds=delay, dry_run=dry_run)
    out = {
        "task_id": task_id,
        "ok": ok,
        "status": status,
        "team_id": meta["team_id"],
        "bot": meta["bot"],
        "source": meta["items"]["source"],
        "message_len": len(msg),
        "dry_run": dry_run,
    }
    print(json.dumps(out, ensure_ascii=False))
    return 0 if ok or status == "already-notified" else 1


def main() -> int:
    parser = argparse.ArgumentParser(description="봇 → 아누 후속 알림 메시지 추출/포맷/발송")
    sub = parser.add_subparsers(dest="cmd", required=True)

    p_ext = sub.add_parser("extract", help="followup 항목 dict JSON 출력")
    p_ext.add_argument("task_id")

    p_fmt = sub.add_parser("format", help="cron 메시지 텍스트 출력")
    p_fmt.add_argument("task_id")

    p_send = sub.add_parser("send", help="cokacdir cron 발송 + .anu-notified 마커 생성")
    p_send.add_argument("task_id")
    p_send.add_argument("--dry-run", action="store_true")
    p_send.add_argument("--delay", type=int, default=10, help="발송 지연 초 (default: 10)")

    args = parser.parse_args()
    if args.cmd == "extract":
        return _cmd_extract(args.task_id)
    if args.cmd == "format":
        return _cmd_format(args.task_id)
    if args.cmd == "send":
        return _cmd_send(args.task_id, args.dry_run, args.delay)
    return 2


if __name__ == "__main__":
    sys.exit(main())
