#!/usr/bin/env python3
"""note-link.py — Obsidian 笔记源文件关联向导

从笔记内容提取关键词，在代码库中搜索匹配的源文件，
展示候选列表供用户确认，写入 source-files frontmatter。

用法:
  python3 note-link.py <note-path>                    # 默认搜索 ~/atdc + cwd
  python3 note-link.py <note-path> --dir ~/my-project # 追加搜索目录
  python3 note-link.py <note-path> --json             # JSON 输出候选（不写入）
"""

import hashlib
import os
import re
import sys
from pathlib import Path


def compute_hash(file_path: str) -> str:
    try:
        with open(file_path, 'rb') as f:
            return hashlib.sha256(f.read()).hexdigest()[:8]
    except (OSError, PermissionError):
        return ""


# ── 关键词提取 ──

_H2_RE = re.compile(r'^##\s+(.+)', re.MULTILINE)
_CODE_PATH_RE = re.compile(r'[`"](/[^\s`"]+\.\w+)[`"]')
_BACKTICK_RE = re.compile(r'`([^`\s]{3,})`')


def extract_keywords(note_path: str) -> list:
    """从笔记标题、h2 标题、代码引用提取关键词"""
    content = Path(note_path).read_text(encoding='utf-8')

    keywords = set()

    # h2 标题
    for m in _H2_RE.finditer(content):
        kw = m.group(1).strip().lower()
        if len(kw) > 2:
            keywords.add(kw)

    # 代码引用路径（如 `src/evidence/index.js`）
    for m in _CODE_PATH_RE.finditer(content):
        keywords.add(m.group(1))

    # 反引号关键词（如 `evidence`、`note-sync`）
    for m in _BACKTICK_RE.finditer(content):
        kw = m.group(1)
        if 3 <= len(kw) <= 40 and '/' not in kw:
            keywords.add(kw.lower())

    # 文件名本身
    stem = Path(note_path).stem.lower()
    for part in re.split(r'[-_\s]', stem):
        if len(part) > 2:
            keywords.add(part)

    return sorted(keywords)[:30]


# ── 源文件搜索 ──

_SKIP_DIRS = {'.git', 'node_modules', '__pycache__', '.obsidian', 'dist',
              'build', '.venv', 'vendor', '.cache', 'openspec'}
_CODE_EXTS = {'.py', '.js', '.ts', '.go', '.rs', '.java', '.rb', '.sh',
              '.md', '.yaml', '.yml', '.json', '.toml'}


def search_sources(keywords: list, search_dirs: list, max_results: int = 20) -> list:
    """在指定目录中搜索匹配的源文件"""
    candidates = []  # (path, score, matched_keywords)

    for base_dir in search_dirs:
        base = Path(base_dir).expanduser()
        if not base.exists():
            continue

        for root, dirs, files in os.walk(base):
            dirs[:] = [d for d in dirs if d not in _SKIP_DIRS]
            for fname in files:
                fpath = Path(root) / fname
                ext = fpath.suffix
                if ext not in _CODE_EXTS:
                    continue

                rel = str(fpath.relative_to(base))
                rel_lower = rel.lower()
                fname_lower = fname.lower()

                score = 0
                matched = []
                for kw in keywords:
                    kw_lower = kw.lower()
                    if kw_lower in fname_lower:
                        score += 3
                        matched.append(kw)
                    elif kw_lower in rel_lower:
                        score += 1
                        matched.append(kw)

                if score > 0:
                    candidates.append((str(fpath), score, matched))

    candidates.sort(key=lambda x: -x[1])
    return [
        {"path": c[0], "score": c[1], "matched": c[2]}
        for c in candidates[:max_results]
    ]


# ── Frontmatter 写入 ──

_FM_RE = re.compile(r'^(---\n)(.*?)(\n---\n)', re.DOTALL)


def update_frontmatter(note_path: str, source_files: list):
    """追加 source-files 到 frontmatter（保留原有字段）"""
    content = Path(note_path).read_text(encoding='utf-8')
    today = os.popen('date +%Y-%m-%d').read().strip()

    # 构建 source-files YAML
    sf_lines = ["source-files:"]
    for sf in source_files:
        sf_lines.append(f"  - path: {sf['path']}")
        sf_lines.append(f"    hash: {sf['hash']}")
    sf_lines.append(f"source-updated: {today}")
    sf_block = '\n'.join(sf_lines)

    match = _FM_RE.match(content)
    if match:
        # 已有 frontmatter → 追加
        prefix = match.group(1)
        fm_raw = match.group(2)
        suffix = match.group(3)

        # 检查是否已有 source-files
        if 'source-files:' in fm_raw:
            print("⚠️  笔记已有 source-files，将追加新条目。")
            # 简单追加（不覆盖已有）
            insert = "\n" + sf_block
            # 插入到 source-files 已有块的后面比较复杂，这里直接追加到 fm 末尾
            new_fm = fm_raw.rstrip() + insert
        else:
            new_fm = fm_raw.rstrip() + "\n\n" + sf_block

        new_content = prefix + new_fm + suffix + content[match.end():]
    else:
        # 无 frontmatter → 创建
        new_content = f"---\n{sf_block}\n---\n\n{content}"

    Path(note_path).write_text(new_content, encoding='utf-8')
    print(f"✅ 已写入 {len(source_files)} 个源文件到 {note_path}")


def main():
    if len(sys.argv) < 2 or sys.argv[1] in ('-h', '--help'):
        print(__doc__)
        sys.exit(0)

    note_path = sys.argv[1]
    if not os.path.exists(note_path):
        print(f"错误: 笔记不存在: {note_path}", file=sys.stderr)
        sys.exit(1)

    json_mode = '--json' in sys.argv

    # 搜索目录
    search_dirs = [os.path.expanduser('~/atdc'), os.getcwd()]
    for i, arg in enumerate(sys.argv):
        if arg == '--dir' and i + 1 < len(sys.argv):
            search_dirs.append(sys.argv[i + 1])

    # 提取关键词
    keywords = extract_keywords(note_path)
    if not json_mode:
        print(f"📝 提取关键词 ({len(keywords)}): {', '.join(keywords[:10])}{'...' if len(keywords) > 10 else ''}")

    # 搜索
    candidates = search_sources(keywords, search_dirs)

    if not candidates:
        print("❌ 未找到匹配的源文件。")
        print("尝试: 手动指定源文件路径，或使用 --dir <path> 扩大搜索范围。")
        sys.exit(0)

    if json_mode:
        import json
        print(json.dumps({"keywords": keywords, "candidates": candidates}, ensure_ascii=False, indent=2))
        return

    # 展示候选
    print(f"\n🔍 找到 {len(candidates)} 个候选源文件:\n")
    for i, c in enumerate(candidates):
        matched_str = ', '.join(c['matched'][:3])
        print(f"  [{i + 1}] (score:{c['score']}) {c['path']}")
        print(f"       匹配: {matched_str}")
    print()

    # 用户确认（非交互模式下全部选中）
    if not sys.stdin.isatty():
        selected = candidates
        print("非交互模式：自动选择全部候选。")
    else:
        print("输入要关联的编号（逗号分隔），或 'all' 选择全部，或 'q' 退出:")
        choice = input("> ").strip()
        if choice == 'q':
            sys.exit(0)
        if choice == 'all':
            selected = candidates
        else:
            indices = [int(x.strip()) - 1 for x in choice.split(',') if x.strip().isdigit()]
            selected = [candidates[i] for i in indices if 0 <= i < len(candidates)]

    if not selected:
        print("未选择任何文件。")
        sys.exit(0)

    # 计算hash + 预览
    print("\n📋 预览将写入的 source-files:")
    to_write = []
    for s in selected:
        h = compute_hash(s['path'])
        to_write.append({"path": s['path'], "hash": h})
        print(f"  {s['path']} (hash: {h})")

    # 确认写入
    if sys.stdin.isatty():
        print("\n确认写入？(y/n)")
        if input("> ").strip().lower() != 'y':
            print("已取消。")
            sys.exit(0)

    update_frontmatter(note_path, to_write)


if __name__ == '__main__':
    main()
