#!/bin/sh
# claude-lessons 安装/更新器 v2（同一条命令，幂等；分层加载防膨胀）
#
#   curl -fsSL https://tools.cooconsbit.com/claude-lessons/install.sh | bash
#   curl -fsSL .../install.sh | bash -s -- --ids batch-rename-collision,verify-before-done
#   curl -fsSL .../install.sh | bash -s -- --categories shell,api --locale en
#   curl -fsSL .../install.sh | bash -s -- --flat        # 全部装进常驻文件（旧行为）
#
# 分层（默认）：
#   hot  条目 → ~/.claude/claude-lessons.md（@import 常驻，每个会话都在）
#   warm 条目 → ~/.claude/skills/claude-lessons-<分类>/SKILL.md
#               （仅 description 一行常驻，正文做相关任务时才被加载——context 几乎零成本）
#
# merge 非覆盖：
#   - 新条目追加；未改动的旧条目按版本升级/随层级迁移；你改过的条目原地保留并提示
#   - 你删除过的条目记入 ~/.claude/claude-lessons.ignore，更新永不回填
#   - claude-lessons.md 中 <!-- local:below --> 之后的内容永不触碰；写入前自动备份 .bak
#   - 你的 CLAUDE.md 仅被幂等插入一行 @import
#
# 环境变量：CLAUDE_LESSONS_BASE 覆盖站点地址；CLAUDE_LESSONS_JSON 用本地 JSON（测试用）
set -eu

BASE="${CLAUDE_LESSONS_BASE:-https://tools.cooconsbit.com}"
IDS=""
CATS=""
LOCALE="zh"
FLAT="0"

while [ $# -gt 0 ]; do
  case "$1" in
    --ids) IDS="$2"; shift 2 ;;
    --ids=*) IDS="${1#--ids=}"; shift ;;
    --categories) CATS="$2"; shift 2 ;;
    --categories=*) CATS="${1#--categories=}"; shift ;;
    --locale) LOCALE="$2"; shift 2 ;;
    --locale=*) LOCALE="${1#--locale=}"; shift ;;
    --flat) FLAT="1"; shift ;;
    *) echo "未知参数: $1" >&2; exit 1 ;;
  esac
done

command -v python3 >/dev/null 2>&1 || { echo "需要 python3（macOS/Linux 自带）"; exit 1; }

TMP_JSON="$(mktemp)"
trap 'rm -f "$TMP_JSON"' EXIT
if [ -n "${CLAUDE_LESSONS_JSON:-}" ]; then
  cp "$CLAUDE_LESSONS_JSON" "$TMP_JSON"
else
  command -v curl >/dev/null 2>&1 || { echo "需要 curl"; exit 1; }
  curl -fsSL "$BASE/claude-lessons.json" -o "$TMP_JSON" || { echo "拉取 $BASE/claude-lessons.json 失败"; exit 1; }
fi

CL_JSON="$TMP_JSON" CL_IDS="$IDS" CL_CATS="$CATS" CL_LOCALE="$LOCALE" CL_FLAT="$FLAT" python3 - <<'PYEOF'
import glob, hashlib, json, os, re, shutil, sys

HOME = os.path.expanduser("~")
CLAUDE_DIR = os.path.join(HOME, ".claude")
SKILLS_DIR = os.path.join(CLAUDE_DIR, "skills")
MANAGED = os.path.join(CLAUDE_DIR, "claude-lessons.md")
STATE = os.path.join(CLAUDE_DIR, "claude-lessons.state")
IGNORE = os.path.join(CLAUDE_DIR, "claude-lessons.ignore")
CLAUDE_MD = os.path.join(CLAUDE_DIR, "CLAUDE.md")
LOCAL_MARK = "<!-- local:below -->"
BLOCK_RE = re.compile(
    r"<!-- lesson:([a-z0-9-]+) v(\d+) h:([0-9a-f]{8}) -->\n(.*?)\n<!-- /lesson -->", re.S
)

data = json.load(open(os.environ["CL_JSON"], encoding="utf-8"))
locale = os.environ.get("CL_LOCALE") or "zh"
flat = os.environ.get("CL_FLAT") == "1"
want_ids = set(filter(None, os.environ.get("CL_IDS", "").split(",")))
want_cats = set(filter(None, os.environ.get("CL_CATS", "").split(",")))

catalog = {l["id"]: l for l in data["lessons"]}
cat_meta = data.get("categories", {})
selected = [
    l for l in data["lessons"]
    if (not want_ids and not want_cats)
    or l["id"] in want_ids
    or l["category"] in want_cats
]
if not selected:
    print("按 --ids/--categories 过滤后没有任何条目，检查参数拼写"); sys.exit(1)

def h8(text): return hashlib.sha1(text.encode("utf-8")).hexdigest()[:8]
def rule_of(l): return "- " + (l["rule"].get(locale) or l["rule"]["zh"])
def loc(d): return d.get(locale) or d.get("zh") or ""
def skill_file(cat): return os.path.join(SKILLS_DIR, f"claude-lessons-{cat}", "SKILL.md")
def target_of(l): return "md" if flat or l.get("tier", "hot") == "hot" else l["category"]

os.makedirs(CLAUDE_DIR, exist_ok=True)
state = {"installed": {}}
if os.path.exists(STATE):
    try: state = json.load(open(STATE, encoding="utf-8"))
    except Exception: pass
ignore = set()
if os.path.exists(IGNORE):
    ignore = {ln.strip() for ln in open(IGNORE, encoding="utf-8") if ln.strip()}

# ---- 解析所有受管文件（常驻 md + 各 warm skill）----
blocks, local_tail = {}, ""
def parse_file(path, src):
    if not os.path.exists(path): return ""
    text = open(path, encoding="utf-8").read()
    for m in BLOCK_RE.finditer(text):
        blocks[m.group(1)] = {
            "version": int(m.group(2)), "hash": m.group(3),
            "content": m.group(4), "src": src,
        }
    return text

md_text = parse_file(MANAGED, "md")
if LOCAL_MARK in md_text:
    local_tail = md_text.split(LOCAL_MARK, 1)[1]
for path in glob.glob(os.path.join(SKILLS_DIR, "claude-lessons-*", "SKILL.md")):
    cat = os.path.basename(os.path.dirname(path)).replace("claude-lessons-", "")
    parse_file(path, cat)

# ---- 删除检测：装过但所有受管文件里都没了 → 进 ignore ----
newly_ignored = []
for lid in list(state["installed"].keys()):
    if lid not in blocks:
        if lid not in ignore:
            ignore.add(lid); newly_ignored.append(lid)
        del state["installed"][lid]

# ---- merge ----
added, updated_ids, moved, conflicts, skipped_ignore = [], [], [], [], []
for l in selected:
    lid = l["id"]
    if lid in ignore:
        skipped_ignore.append(lid); continue
    content, target = rule_of(l), target_of(l)
    if lid in blocks:
        b = blocks[lid]
        if h8(b["content"]) != b["hash"]:
            conflicts.append(lid); continue          # 用户改过 → 原地保留，不动不迁
        if l["version"] > b["version"]:
            b.update(version=l["version"], hash=h8(content), content=content)
            updated_ids.append(lid)
        if b["src"] != target:
            b["src"] = target; moved.append(lid)     # 层级/flat 切换迁移
    else:
        blocks[lid] = {"version": l["version"], "hash": h8(content), "content": content, "src": target}
        added.append(lid)
    state["installed"][lid] = l["version"]

def backup_write(path, content):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    if os.path.exists(path):
        shutil.copyfile(path, path + ".bak")
    open(path, "w", encoding="utf-8").write(content)

def emit_blocks(ids):
    out = []
    for lid in ids:
        b = blocks[lid]
        out += [f'<!-- lesson:{lid} v{b["version"]} h:{b["hash"]} -->', b["content"], "<!-- /lesson -->"]
    return out

def ids_in_src(src):
    ordered = [l["id"] for l in data["lessons"] if l["id"] in blocks and blocks[l["id"]]["src"] == src]
    orphans = [lid for lid in blocks if blocks[lid]["src"] == src and lid not in catalog]
    return ordered, orphans

# ---- 重建常驻 md ----
hint = os.environ.get("CL_BASE_HINT", "https://tools.cooconsbit.com")
lines = [
    f'<!-- claude-lessons v{data["version"]} | {data.get("source", "")} -->',
    f"<!-- 更新: curl -fsSL {hint}/claude-lessons/install.sh | bash -->",
    "<!-- 手动补充写在文末 local:below 之后；删除某条则该条不再回填 -->",
    "",
    "# Claude 踩坑守则（claude-lessons）",
    "",
]
for cat, meta in cat_meta.items():
    cat_ids = [l["id"] for l in data["lessons"]
               if l["category"] == cat and l["id"] in blocks and blocks[l["id"]]["src"] == "md"]
    if not cat_ids: continue
    lines.append(f'## {meta.get("emoji", "")} {loc(meta)}'.strip())
    lines += emit_blocks(cat_ids)
    lines.append("")
_, md_orphans = ids_in_src("md")
if md_orphans:
    lines.append("## 其他")
    lines += emit_blocks(md_orphans)
    lines.append("")
lines.append(LOCAL_MARK)
lines.append(local_tail.rstrip("\n") if local_tail.strip() else "")
backup_write(MANAGED, "\n".join(lines).rstrip("\n") + "\n")

# ---- 重建各 warm skill 文件 ----
skills_written, skills_removed = [], []
all_cats = set(cat_meta.keys()) | {b["src"] for b in blocks.values() if b["src"] != "md"}
for cat in all_cats:
    path = skill_file(cat)
    ordered, orphans = ids_in_src(cat)
    ids = ordered + orphans
    if not ids:
        if os.path.exists(path):  # 该 skill 已无任何条目 → 清理受管产物
            shutil.rmtree(os.path.dirname(path))
            skills_removed.append(f"claude-lessons-{cat}")
        continue
    meta = cat_meta.get(cat, {})
    desc = loc(meta.get("skillDesc", {})) or f"claude-lessons {cat} rules"
    body = [
        "---",
        f"name: claude-lessons-{cat}",
        f"description: {desc}",
        "---",
        "",
        f'# {loc(meta) or cat}（claude-lessons，按需加载）',
        "",
    ] + emit_blocks(ids) + [""]
    backup_write(path, "\n".join(body))
    skills_written.append(f"claude-lessons-{cat}({len(ids)})")

json.dump(state, open(STATE, "w", encoding="utf-8"), indent=1)
if ignore:
    open(IGNORE, "w", encoding="utf-8").write("\n".join(sorted(ignore)) + "\n")

# ---- CLAUDE.md 幂等插入 @import ----
import_block = "\n<!-- claude-lessons:import -->\n@~/.claude/claude-lessons.md\n<!-- /claude-lessons:import -->\n"
md = open(CLAUDE_MD, encoding="utf-8").read() if os.path.exists(CLAUDE_MD) else ""
import_added = "claude-lessons:import" not in md
if import_added:
    open(CLAUDE_MD, "a", encoding="utf-8").write(import_block)

hot_count = sum(1 for b in blocks.values() if b["src"] == "md")
warm_count = len(blocks) - hot_count
hot_size = sum(len(b["content"]) + 70 for b in blocks.values() if b["src"] == "md") + 220
print(f"✅ claude-lessons v{data['version']} 合并完成")
print(f"   新增 {len(added)} · 升级 {len(updated_ids)} · 迁移 {len(moved)} · 共 {len(blocks)} 条")
print(f"   常驻(hot) {hot_count} 条 ≈ {hot_size // 3} tokens/会话 · 按需(warm) {warm_count} 条（正文零常驻成本）")
if skills_written: print(f"   skills: {', '.join(sorted(skills_written))}")
if skills_removed: print(f"   清理空 skill: {', '.join(skills_removed)}")
if import_added: print("   已在 ~/.claude/CLAUDE.md 插入 @import（卸载 = 删该行 + 删受管文件/skills）")
if conflicts: print(f"   ⚠️ 保留你改过的 {len(conflicts)} 条（未覆盖/未迁移）: {', '.join(conflicts)}")
if newly_ignored: print(f"   🗑 检测到你删除过: {', '.join(newly_ignored)} → 已记入 ignore，不再回填")
if skipped_ignore: print(f"   ⏭ 按 ignore 跳过: {', '.join(skipped_ignore)}")
PYEOF
