#!/usr/bin/env python3
"""Import bid-analysis related notes from Youdao Note into this repository."""

from __future__ import annotations

import argparse
import datetime as dt
import json
import re
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path


DEFAULT_KEYWORDS = [
    "招投标",
    "招投标分析",
    "投标分析",
    "招标分析",
    "标书分析",
    "中标分析",
    "智慧食堂招标",
    "团餐招标",
    "团餐挂网",
    "挂网项目",
    "招标文件",
    "技术参数分析",
    "中标金额",
    "产品策略",
]

INCLUDE_TERMS = ("招投标", "招标", "投标", "中标", "挂网", "标书", "政府采购")
ANALYSIS_TERMS = (
    "分析",
    "策略",
    "产品",
    "技术参数",
    "品牌",
    "金额",
    "行业",
    "市场",
    "竞品",
    "智慧食堂",
    "团餐",
    "易方报",
)
EXCLUDE_TERMS = (
    "招聘",
    "风投",
    "直投",
    "私募",
    "ipo",
    "反向收购",
    "六投三不投",
    "投人",
    "源码",
    "Java",
    "静态代码",
    "漏斗分析",
    "涉众分析",
    "需求分析",
)


@dataclass
class NoteHit:
    file_id: str
    title: str
    kind: str
    keywords: set[str] = field(default_factory=set)
    include_reason: str = ""
    status: str = "candidate"
    output_path: str | None = None
    content_chars: int = 0
    error: str | None = None


def run_youdao(args: list[str]) -> str:
    completed = subprocess.run(
        ["youdaonote", *args],
        check=False,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )
    if completed.returncode != 0:
        detail = completed.stderr.strip() or completed.stdout.strip()
        raise RuntimeError(f"youdaonote {' '.join(args)} failed: {detail}")
    return completed.stdout


def compact_error(error: str, limit: int = 240) -> str:
    message_match = re.search(r'"message"\s*:\s*"([^"]+)"', error)
    if message_match:
        error = message_match.group(1)
    error = re.sub(r"\s+", " ", error).strip()
    if len(error) > limit:
        return error[: limit - 3].rstrip() + "..."
    return error


def parse_search_output(output: str, keyword: str) -> list[NoteHit]:
    hits: list[NoteHit] = []
    for raw_line in output.splitlines():
        line = raw_line.strip()
        if not line:
            continue
        match = re.match(r"^(?P<kind>[^\s]+)\s+(?P<id>\S+)\s+(?P<title>.+)$", line)
        if not match:
            continue
        kind = match.group("kind")
        if kind != "📄":
            continue
        title = match.group("title").strip()
        hit = NoteHit(file_id=match.group("id"), title=title, kind=kind)
        hit.keywords.add(keyword)
        hits.append(hit)
    return hits


def classify_title(title: str) -> tuple[bool, str]:
    lower_title = title.lower()
    if any(term.lower() in lower_title for term in EXCLUDE_TERMS):
        return False, "title_excluded"
    has_bid_term = any(term in title for term in INCLUDE_TERMS)
    has_analysis_term = any(term in title for term in ANALYSIS_TERMS)
    if "招投标" in title:
        return True, "title_contains_招投标"
    if has_bid_term and has_analysis_term:
        return True, "title_contains_bid_and_analysis_terms"
    if "智慧食堂" in title and ("行业" in title or "金额" in title or "市场分析" in title):
        return True, "title_contains_smart_canteen_market_analysis"
    return False, "low_confidence_title"


def slugify(title: str, file_id: str) -> str:
    cleaned = re.sub(r"\.(note|clip)$", "", title, flags=re.IGNORECASE)
    cleaned = re.sub(r"[\\/:*?\"<>|#`]+", "-", cleaned)
    cleaned = re.sub(r"\s+", "-", cleaned).strip("-")
    cleaned = cleaned[:80].strip("-") or file_id
    return f"{cleaned}-{file_id[:8]}.md"


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


def write_article(path: Path, hit: NoteHit, content: str, imported_at: str) -> None:
    keywords = ", ".join(sorted(hit.keywords))
    front_matter = [
        "---",
        f"title: {yaml_scalar(hit.title)}",
        f"youdao_file_id: {yaml_scalar(hit.file_id)}",
        f"matched_keywords: {yaml_scalar(keywords)}",
        f"imported_at: {yaml_scalar(imported_at)}",
        f"include_reason: {yaml_scalar(hit.include_reason)}",
        "---",
        "",
    ]
    path.write_text("\n".join(front_matter) + content.strip() + "\n", encoding="utf-8")


def write_index(library_dir: Path, imported: list[NoteHit], candidates: list[NoteHit], imported_at: str) -> None:
    article_lines = [
        "# 有道云招投标分析文章库",
        "",
        f"导入时间：{imported_at}",
        "",
        "## 范围",
        "",
        "本目录从有道云笔记检索并沉淀招投标分析相关资料，作为后续抽取项目 Skill 和 Workflow 的来源库。",
        "",
        "当前导入规则优先收录标题同时包含招投标/招标/投标/中标/挂网等采购语义，以及分析/策略/产品/技术参数/市场等方法论语义的笔记。",
        "",
        "## 已收录文章",
        "",
    ]
    if imported:
        for hit in imported:
            filename = Path(hit.output_path or "").name
            article_lines.append(
                f"- [{hit.title}](articles/{filename}) - `{hit.file_id}`, {hit.content_chars} 字符，关键词：{', '.join(sorted(hit.keywords))}"
            )
    else:
        article_lines.append("- 暂无成功读取的文章。")

    article_lines.extend(
        [
            "",
            "## 辅助文件",
            "",
            "- `manifest.json`：机器可读导入清单。",
            "- `search-results.md`：所有关键词命中结果和分类状态。",
            "- `candidates.md`：低置信候选、空正文或读取失败条目。",
            "",
            "## 下一步抽取建议",
            "",
            "1. 先对 `articles/` 中每篇文章做结构化摘要：业务场景、数据口径、分析步骤、输出物、可复用提示词。",
            "2. 把跨文章重复出现的分析步骤沉淀为 `workflows/` 下的流程文档。",
            "3. 把可被代理直接执行的步骤抽成 Skill：输入要求、命令、验收标准、失败恢复方式。",
            "",
            f"候选/异常条目数：{len(candidates)}",
        ]
    )
    (library_dir / "README.md").write_text("\n".join(article_lines) + "\n", encoding="utf-8")


def write_search_results(library_dir: Path, hits: list[NoteHit], search_errors: dict[str, str]) -> None:
    lines = [
        "# 有道云检索结果",
        "",
        "## 关键词异常",
        "",
    ]
    if search_errors:
        for keyword, error in sorted(search_errors.items()):
            escaped_error = error.replace("\n", " ")
            lines.append(f"- `{keyword}`: {escaped_error}")
    else:
        lines.append("- 无。")
    lines.extend(
        [
            "",
            "## 命中条目",
            "",
            "| 状态 | 标题 | Youdao ID | 关键词 | 原因 |",
            "| --- | --- | --- | --- | --- |",
        ]
    )
    for hit in sorted(hits, key=lambda item: (item.status, item.title)):
        title = hit.title.replace("|", "\\|")
        keywords = ", ".join(sorted(hit.keywords)).replace("|", "\\|")
        reason = (hit.error or hit.include_reason).replace("|", "\\|")
        lines.append(f"| {hit.status} | {title} | `{hit.file_id}` | {keywords} | {reason} |")
    (library_dir / "search-results.md").write_text("\n".join(lines) + "\n", encoding="utf-8")


def write_candidates(library_dir: Path, candidates: list[NoteHit]) -> None:
    lines = [
        "# 候选与异常条目",
        "",
        "这些条目被关键词命中，但未进入正文文章库。后续人工确认后，可调整导入规则或直接按 Youdao ID 读取。",
        "",
        "| 状态 | 标题 | Youdao ID | 关键词 | 原因 |",
        "| --- | --- | --- | --- | --- |",
    ]
    for hit in sorted(candidates, key=lambda item: (item.status, item.title)):
        title = hit.title.replace("|", "\\|")
        keywords = ", ".join(sorted(hit.keywords)).replace("|", "\\|")
        reason = (hit.error or hit.include_reason).replace("|", "\\|")
        lines.append(f"| {hit.status} | {title} | `{hit.file_id}` | {keywords} | {reason} |")
    (library_dir / "candidates.md").write_text("\n".join(lines) + "\n", encoding="utf-8")


def write_manifest(
    library_dir: Path,
    hits: list[NoteHit],
    keywords: list[str],
    imported_at: str,
    search_errors: dict[str, str],
) -> None:
    data = {
        "imported_at": imported_at,
        "keywords": keywords,
        "search_errors": search_errors,
        "counts": {
            "total_hits": len(hits),
            "imported": sum(1 for hit in hits if hit.status == "imported"),
            "candidate": sum(1 for hit in hits if hit.status == "candidate"),
            "empty": sum(1 for hit in hits if hit.status == "empty"),
            "error": sum(1 for hit in hits if hit.status == "error"),
        },
        "notes": [
            {
                "file_id": hit.file_id,
                "title": hit.title,
                "status": hit.status,
                "matched_keywords": sorted(hit.keywords),
                "include_reason": hit.include_reason,
                "content_chars": hit.content_chars,
                "output_path": hit.output_path,
                "error": hit.error,
            }
            for hit in sorted(hits, key=lambda item: item.title)
        ],
    }
    (library_dir / "manifest.json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def import_articles(repo_root: Path, keywords: list[str], min_chars: int) -> int:
    library_dir = repo_root / "docs" / "article-library" / "youdao-bid-analysis"
    articles_dir = library_dir / "articles"
    articles_dir.mkdir(parents=True, exist_ok=True)

    hits_by_id: dict[str, NoteHit] = {}
    search_errors: dict[str, str] = {}
    for keyword in keywords:
        try:
            output = run_youdao(["search", keyword])
        except Exception as exc:  # noqa: BLE001
            search_errors[keyword] = compact_error(str(exc))
            continue
        for hit in parse_search_output(output, keyword):
            if hit.file_id in hits_by_id:
                hits_by_id[hit.file_id].keywords.update(hit.keywords)
            else:
                hits_by_id[hit.file_id] = hit

    imported_at = dt.datetime.now(dt.timezone(dt.timedelta(hours=8))).isoformat(timespec="seconds")
    all_hits = list(hits_by_id.values())

    for hit in all_hits:
        include, reason = classify_title(hit.title)
        hit.include_reason = reason
        if not include:
            hit.status = "candidate"
            continue
        try:
            content = run_youdao(["read", hit.file_id])
        except Exception as exc:  # noqa: BLE001
            hit.status = "error"
            hit.error = compact_error(str(exc))
            continue
        content = content.strip()
        hit.content_chars = len(content)
        if len(content) < min_chars:
            hit.status = "empty"
            hit.error = f"read returned only {len(content)} chars"
            continue
        filename = slugify(hit.title, hit.file_id)
        output_path = articles_dir / filename
        write_article(output_path, hit, content, imported_at)
        hit.status = "imported"
        hit.output_path = str(output_path.relative_to(repo_root))

    imported = [hit for hit in all_hits if hit.status == "imported"]
    candidates = [hit for hit in all_hits if hit.status != "imported"]
    write_index(library_dir, imported, candidates, imported_at)
    write_search_results(library_dir, all_hits, search_errors)
    write_candidates(library_dir, candidates)
    write_manifest(library_dir, all_hits, keywords, imported_at, search_errors)

    print(f"Imported {len(imported)} articles into {articles_dir}")
    print(f"Tracked {len(candidates)} candidates/errors in {library_dir / 'candidates.md'}")
    if search_errors:
        print(f"Search errors: {len(search_errors)} keyword(s); see search-results.md")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--repo-root",
        default=Path(__file__).resolve().parents[1],
        type=Path,
        help="Repository root. Defaults to the parent of scripts/.",
    )
    parser.add_argument(
        "--keyword",
        action="append",
        dest="keywords",
        help="Search keyword. May be passed multiple times. Defaults to built-in bid-analysis terms.",
    )
    parser.add_argument(
        "--min-chars",
        type=int,
        default=100,
        help="Minimum note body length to keep as an imported article.",
    )
    args = parser.parse_args()
    return import_articles(args.repo_root.resolve(), args.keywords or DEFAULT_KEYWORDS, args.min_chars)


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