#!/usr/bin/env python3
"""Export sanitized zhctprompt Codex conversations into the shared repository."""

from __future__ import annotations

import argparse
from datetime import date
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import sys
from typing import Any


REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_OUTPUT_ROOT = REPO_ROOT / "work/team-learning/codex-conversation-archive"
DEFAULT_ARCHIVE_INDEX = (
    Path.home()
    / "code/099-github/notebooklm-mcp/output/codex-thread-archives/latest/index.json"
)


SENSITIVE_PATTERNS = (
    re.compile(r"(?i)\b(?:bearer)\s+[A-Za-z0-9._~+/=-]{12,}"),
    re.compile(
        r"(?i)((?:账号|用户名|用户密码|密码|手机号|手机|host|hostname|username|db[_-]?user|"
        r"token|api[_-]?key|secret|password|passwd|pwd|access[_-]?token|refresh[_-]?token|"
        r"cookie|session|note[_ -]?id|file[_ -]?id|thread[_ -]?id|session[_ -]?id|"
        r"会话[ _-]?id|文件[ _-]?id)\s*(?:(?:是\s*)?[：:=]\s*|是\s+))"
        r"[`*_]*([^\s,，;；]+)"
    ),
    re.compile(r"(?<![A-Za-z0-9])1[3-9]\d{9}(?![A-Za-z0-9])"),
    re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.IGNORECASE),
    re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)"),
    re.compile(r"(?i)\b(?:sk|gsk|ak|key|token)[-_][A-Za-z0-9._-]{12,}\b"),
    re.compile(r"\beyJ[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]{8,}){1,2}\b"),
    re.compile(r"(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b"),
)


RESTRICTED_CONTEXT_GROUPS = {
    "credentials-or-infrastructure": (
        "账号",
        "密码",
        "凭据",
        "cookie",
        "token",
        "secret",
        "password",
        "数据库",
        "生产环境",
        "生产日志",
        "线上日志",
        "服务器",
        "运维日志",
    ),
    "personal-or-hr": (
        "身份证",
        "人事",
        "员工",
        "试用期",
        "考核表",
        "培养计划",
        "绩效",
        "薪酬",
        "工资",
        "通讯录",
        "个人信息",
        "有道云",
        "企业微信",
        "微盘",
        "云盘",
        "微信聊天",
        "私聊",
        "客户原始",
        "客户资料",
        "客户数据",
    ),
    "finance-or-legal": (
        "报销",
        "付款",
        "收款",
        "发票",
        "银行账户",
        "合同原文",
        "报价单",
    ),
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--archive-index",
        type=Path,
        default=Path(os.environ.get("CODEX_THREAD_ARCHIVE_INDEX", DEFAULT_ARCHIVE_INDEX)),
        help="Path to codex-thread-archiver latest/index.json.",
    )
    parser.add_argument(
        "--workspace",
        default="zhctprompt",
        help="Workspace basename or normalized workspace suffix to export.",
    )
    parser.add_argument(
        "--collector",
        help="Teammate/collector name. Defaults to YUNXIAO_CREATOR_NAME or git user.name.",
    )
    parser.add_argument("--date", default=date.today().isoformat(), help="Manifest date (YYYY-MM-DD).")
    parser.add_argument("--output-root", type=Path, default=DEFAULT_OUTPUT_ROOT)
    parser.add_argument("--dry-run", action="store_true", help="Validate and report without writing.")
    parser.add_argument("--json", action="store_true", help="Print machine-readable result JSON.")
    return parser.parse_args()


def resolve_collector(explicit: str | None) -> str:
    if explicit and explicit.strip():
        return explicit.strip()
    env_name = os.environ.get("YUNXIAO_CREATOR_NAME", "").strip()
    if env_name:
        return env_name
    result = subprocess.run(
        ["git", "config", "user.name"],
        cwd=REPO_ROOT,
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.DEVNULL,
    )
    name = result.stdout.strip()
    if not name:
        raise ValueError("collector is required: pass --collector or configure git user.name")
    return name


def slugify_collector(name: str) -> str:
    ascii_slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
    if ascii_slug:
        return ascii_slug
    digest = hashlib.sha256(name.encode("utf-8")).hexdigest()[:10]
    return f"collector-{digest}"


def sanitize_text(value: Any) -> tuple[str, int]:
    text = str(value or "")
    replacements = 0

    home_pattern = re.compile(r"/Users/[^/\s]+")
    text, count = home_pattern.subn("$HOME", text)
    replacements += count

    temp_path_pattern = re.compile(r"/(?:var/folders|private/var/folders|tmp)/[^\s\"'<>]+")
    text, count = temp_path_pattern.subn("[REDACTED_LOCAL_PATH]", text)
    replacements += count

    rds_host_pattern = re.compile(r"(?i)\b[a-z0-9-]+\.mysql\.rds\.aliyuncs\.com\b")
    text, count = rds_host_pattern.subn("[REDACTED_DB_HOST]", text)
    replacements += count

    mysql_command_pattern = re.compile(
        r"(?i)mysql\s+-h\s+\S+(?:\s+-P\s*\d+)?(?:\s+-u\s*\S+)?(?:\s+-p\S*)?"
    )
    text, count = mysql_command_pattern.subn("mysql [REDACTED_CONNECTION_COMMAND]", text)
    replacements += count

    url_credentials = re.compile(r"(?i)([a-z][a-z0-9+.-]*://)([^\s/@:]+):([^\s/@]+)@")
    text, count = url_credentials.subn(r"\1[REDACTED_USER]:[REDACTED_SECRET]@", text)
    replacements += count

    for index, pattern in enumerate(SENSITIVE_PATTERNS):
        if index == 1:
            text, count = pattern.subn(r"\1[REDACTED]", text)
        elif index == 0:
            text, count = pattern.subn("Bearer [REDACTED]", text)
        elif index == 2:
            text, count = pattern.subn("[REDACTED_PHONE]", text)
        elif index == 3:
            text, count = pattern.subn("[REDACTED_EMAIL]", text)
        elif index == 4:
            text, count = pattern.subn("[REDACTED_IP]", text)
        elif index == len(SENSITIVE_PATTERNS) - 1:
            text, count = pattern.subn("[REDACTED_ID]", text)
        else:
            text, count = pattern.subn("[REDACTED_SECRET]", text)
        replacements += count

    return " ".join(text.split()), replacements


def yaml_quote(value: str) -> str:
    return json.dumps(value, ensure_ascii=False)


def conversation_key(thread_id: str) -> str:
    return hashlib.sha256(thread_id.encode("utf-8")).hexdigest()[:16]


def matches_workspace(thread: dict[str, Any], workspace: str) -> bool:
    expected = workspace.strip("/")
    candidate = str(thread.get("workspace") or "").strip("/")
    if candidate:
        return candidate == expected or candidate.endswith(f"/{expected}")
    cwd_name = Path(str(thread.get("cwd") or "/")).name
    return cwd_name == expected


def safe_value(value: Any) -> tuple[str, int]:
    return sanitize_text(value)


def restricted_context(thread: dict[str, Any]) -> str | None:
    values: list[str] = []
    for field in ("title", "goal", "progress"):
        values.append(str(thread.get(field) or ""))
    values.extend(str(item) for item in (thread.get("plan") or []))
    values.extend(str(item.get("message") or "") for item in (thread.get("timeline") or []))
    searchable = "\n".join(values).lower()
    for group, keywords in RESTRICTED_CONTEXT_GROUPS.items():
        if any(keyword.lower() in searchable for keyword in keywords):
            return group
    return None


def render_thread(
    thread: dict[str, Any], collector: str, key: str, restriction: str | None = None
) -> tuple[str, int]:
    redactions = 0

    def clean(value: Any) -> str:
        nonlocal redactions
        result, count = safe_value(value)
        redactions += count
        return result

    title = clean(thread.get("title") or "Untitled conversation")
    workspace = clean(thread.get("workspace"))
    stage = clean(thread.get("stage") or "unknown")
    source = clean(thread.get("source") or "unknown")
    started_at = clean(thread.get("started_at"))
    updated_at = clean(thread.get("updated_at"))
    if restriction:
        title = f"受限会话 {key}"
        goal = "本会话被隐私门禁判定为高风险内容，正文未进入共享 Git。"
        progress = "仅保留会话存在性、时间、阶段和消息数量；需要查看原文时，应回到授权人员的本机 Codex 数据层。"
    else:
        goal = clean(thread.get("goal") or title)
        progress = clean(thread.get("progress") or "No progress summary available.")

    plan_lines = []
    for item in thread.get("plan") or []:
        if restriction:
            break
        plan_lines.append(f"- {clean(item)}")
    if not plan_lines:
        plan_lines.append(
            "- 受限内容未进入共享 Git。" if restriction else "- No structured plan captured."
        )

    timeline_lines = []
    for item in thread.get("timeline") or []:
        timestamp = clean(item.get("timestamp"))
        role = clean(item.get("role") or "unknown")
        message = "[受限内容未进入共享 Git]" if restriction else clean(item.get("message"))
        timeline_lines.append(f"- `{timestamp}` `{role}`: {message}")
    if not timeline_lines:
        timeline_lines.append("- No condensed timeline available.")

    body = [
        "---",
        f"conversation_key: {yaml_quote(key)}",
        f"collector: {yaml_quote(clean(collector))}",
        f"title: {yaml_quote(title)}",
        f"workspace: {yaml_quote(workspace)}",
        f"stage: {yaml_quote(stage)}",
        f"source: {yaml_quote(source)}",
        f"started_at: {yaml_quote(started_at)}",
        f"updated_at: {yaml_quote(updated_at)}",
        f"privacy: {yaml_quote('restricted-metadata-only' if restriction else 'sanitized-summary-only')}",
        "---",
        "",
        f"# {title}",
        "",
        f"- 采集人：`{clean(collector)}`",
        f"- 工作空间：`{workspace}`",
        f"- 阶段：`{stage}`",
        f"- 开始时间：`{started_at}`",
        f"- 最近更新：`{updated_at}`",
        "- 边界：受限会话只保存存在性和消息结构；其他会话仅保存脱敏后的目标、计划、进展和压缩对话。不保存原始 transcript、凭据、Cookie、生产 payload、个人本地路径或原始会话 ID。",
        "",
        "## 目标",
        "",
        goal,
        "",
        "## 计划",
        "",
        *plan_lines,
        "",
        "## 当前进展",
        "",
        progress,
        "",
        "## 脱敏对话时间线",
        "",
        *timeline_lines,
        "",
    ]
    return "\n".join(body), redactions


def load_threads(index_path: Path) -> tuple[str, list[dict[str, Any]]]:
    if not index_path.is_file():
        raise FileNotFoundError(f"archive index not found: {index_path}")
    payload = json.loads(index_path.read_text(encoding="utf-8"))
    threads = payload.get("threads")
    if not isinstance(threads, list):
        raise ValueError("archive index does not contain a threads list")
    return str(payload.get("generated_at") or ""), threads


def write_if_changed(path: Path, content: str, dry_run: bool) -> str:
    existed = path.exists()
    if existed and path.read_text(encoding="utf-8") == content:
        return "unchanged"
    if not dry_run:
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(content, encoding="utf-8")
    return "updated" if existed else "created"


def export_conversations(args: argparse.Namespace) -> dict[str, Any]:
    collector = resolve_collector(args.collector)
    collector_slug = slugify_collector(collector)
    generated_at, all_threads = load_threads(args.archive_index)
    matched = [thread for thread in all_threads if matches_workspace(thread, args.workspace)]

    missing_ids = [thread.get("title") for thread in matched if not thread.get("thread_id")]
    if missing_ids:
        raise ValueError(f"matched conversations without thread_id: {missing_ids[:3]}")

    output_root = args.output_root.resolve()
    thread_root = output_root / "collectors" / collector_slug / "threads"
    rows: list[dict[str, str]] = []
    daily_sections: list[dict[str, Any]] = []
    counts = {"created": 0, "updated": 0, "unchanged": 0}
    redactions = 0
    restricted_count = 0

    for thread in sorted(matched, key=lambda item: str(item.get("updated_at") or "")):
        key = conversation_key(str(thread["thread_id"]))
        restriction = restricted_context(thread)
        if restriction:
            restricted_count += 1
        content, item_redactions = render_thread(thread, collector, key, restriction)
        redactions += item_redactions
        path = thread_root / f"{key}.md"
        status = write_if_changed(path, content, args.dry_run)
        counts[status] += 1
        title, _ = sanitize_text(thread.get("title") or "Untitled conversation")
        if restriction:
            title = f"受限会话 {key}"
        stage, _ = sanitize_text(thread.get("stage") or "unknown")
        updated_at, _ = sanitize_text(thread.get("updated_at") or "")
        rows.append(
            {
                "key": key,
                "title": title,
                "stage": stage,
                "updated_at": updated_at,
                "path": str(path.relative_to(output_root)),
                "status": status,
            }
        )

        daily_messages: list[dict[str, str]] = []
        for item in thread.get("timeline") or []:
            timestamp, timestamp_redactions = sanitize_text(item.get("timestamp") or "")
            if not timestamp.startswith(args.date):
                continue
            role, role_redactions = sanitize_text(item.get("role") or "unknown")
            if restriction:
                message = "[受限内容未进入共享 Git]"
                message_redactions = 0
            else:
                message, message_redactions = sanitize_text(item.get("message") or "")
            redactions += timestamp_redactions + role_redactions + message_redactions
            daily_messages.append({"timestamp": timestamp, "role": role, "message": message})
        if daily_messages:
            daily_sections.append(
                {
                    "key": key,
                    "title": title,
                    "stage": stage,
                    "updated_at": updated_at,
                    "path": str(path.relative_to(output_root)),
                    "messages": daily_messages,
                    "restricted": bool(restriction),
                }
            )

    daily_question_count = sum(
        1
        for section in daily_sections
        for message in section["messages"]
        if message["role"] == "user"
    )
    daily_answer_count = sum(
        1
        for section in daily_sections
        for message in section["messages"]
        if message["role"] == "assistant"
    )
    daily_message_count = sum(len(section["messages"]) for section in daily_sections)

    manifest_lines = [
        f"# 团队 Codex 每日问答归档｜{args.date}｜{collector}",
        "",
        f"- 来源生成时间：`{generated_at}`",
        f"- 工作空间过滤：`{args.workspace}`",
        f"- 本机可见且匹配的历史会话：`{len(matched)}`；已生成会话 Markdown：`{len(rows)}`",
        f"- 新建：`{counts['created']}`；更新：`{counts['updated']}`；未变化：`{counts['unchanged']}`",
        f"- 当日有问答的会话：`{len(daily_sections)}`；用户提问：`{daily_question_count}`；助手回应：`{daily_answer_count}`；消息合计：`{daily_message_count}`",
        f"- 脱敏替换次数：`{redactions}`",
        f"- 受限会话：`{restricted_count}`（只保留存在性、时间、角色和消息数量，不保存正文）",
        "- 完整性规则：匹配数必须等于成功生成的 Markdown 数；任一会话缺少稳定标识或输出失败时任务失败，不得宣布全员完整。",
        "- 跨电脑边界：本清单只证明当前采集人电脑上可见且属于该工作空间的会话；其他同事必须在各自电脑运行同一任务并提交自己的采集目录。",
        "",
        "## 当日会话索引",
        "",
        "| 会话键 | 标题 | 阶段 | 最近更新 | 会话 Markdown | 当日消息数 |",
        "| --- | --- | --- | --- | --- | ---: |",
    ]
    for section in sorted(daily_sections, key=lambda item: item["updated_at"], reverse=True):
        safe_title = section["title"].replace("|", "\\|")
        manifest_lines.append(
            f"| `{section['key']}` | {safe_title} | `{section['stage']}` | `{section['updated_at']}` | "
            f"[{section['path']}](../../{section['path']}) | {len(section['messages'])} |"
        )
    if not daily_sections:
        manifest_lines.append("| — | 当日未发现本机可见的匹配问答 | — | — | — | 0 |")

    manifest_lines.extend(["", "## 当日问答", ""])
    for section in sorted(daily_sections, key=lambda item: item["updated_at"]):
        manifest_lines.extend(
            [
                f"### {section['title']}",
                "",
                f"- 会话键：`{section['key']}`",
                f"- 明细：[{section['path']}](../../{section['path']})",
                "",
            ]
        )
        for message in section["messages"]:
            role_label = "提问" if message["role"] == "user" else "回应"
            manifest_lines.append(
                f"- `{message['timestamp']}` **{role_label}**：{message['message']}"
            )
        manifest_lines.append("")
    manifest_path = output_root / "daily" / args.date / f"{collector_slug}.md"
    manifest_status = write_if_changed(manifest_path, "\n".join(manifest_lines), args.dry_run)

    return {
        "archive_generated_at": generated_at,
        "collector": collector,
        "collector_slug": collector_slug,
        "workspace": args.workspace,
        "matched_count": len(matched),
        "exported_count": len(rows),
        "redaction_count": redactions,
        "restricted_count": restricted_count,
        "daily_conversation_count": len(daily_sections),
        "daily_question_count": daily_question_count,
        "daily_answer_count": daily_answer_count,
        "daily_message_count": daily_message_count,
        "counts": counts,
        "manifest_status": manifest_status,
        "manifest_path": str(manifest_path),
        "output_root": str(output_root),
        "dry_run": args.dry_run,
    }


def main() -> int:
    args = parse_args()
    try:
        result = export_conversations(args)
    except (OSError, ValueError, json.JSONDecodeError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2
    if result["matched_count"] != result["exported_count"]:
        print("ERROR: matched/exported count mismatch", file=sys.stderr)
        return 3
    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print(result["manifest_path"])
    return 0


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