#!/usr/bin/env python3
"""Claude Code の操作ログを数える。

自分の環境で「実際に何が使われているか」を出すためのスクリプト。
記事「Claude Code の使い方」（https://aidia.pengi-n.co.jp/articles/claude-code-tsukaikata/）の
集計をそのまま再現できる。ログは読み取りのみで、外部送信は一切しない。

使い方:
    python count_tool_calls.py                 # 既定のログ置き場を自動で探す
    python count_tool_calls.py --dir <path>    # 置き場を明示する
    python count_tool_calls.py --days 30       # 直近30日だけ
    python count_tool_calls.py --since 2026-08-05 --until 2026-09-03   # 期間を固定する
    python count_tool_calls.py --csv out.csv   # 集計結果を CSV に書き出す

--days は「今日」を含むため、実行するたびに結果が変わる（当日のログが増え続ける）。
数字を人に見せる・記事に書く・前月と比べるなら、--since / --until で期間を閉じること。

ログの場所（既定）:
    Windows  : %USERPROFILE%\.claude\projects\*\*.jsonl
    mac/Linux: ~/.claude/projects/*/*.jsonl
"""
from __future__ import annotations

import argparse
import collections
import csv
import datetime as dt
import io
import json
import os
import re
import statistics
import sys
from pathlib import Path

# Windows の既定は CP932 なので、明示的に UTF-8 へ切り替える。
for _s in (sys.stdout, sys.stderr):
    if getattr(_s, "encoding", "") and _s.encoding.lower() != "utf-8":
        try:
            _s.reconfigure(encoding="utf-8")
        except Exception:
            pass

# インタプリタが .py ファイルを実行しているものだけを「スクリプト実行」と数える。
# ここを緩めて「コマンドに .py が出てきたら Python」にすると、
# python -c "..."（その場で書き捨てるコード）や git show foo.py（ただの引数）まで混ざり、
# 数字が2倍以上に膨らむ。最初に書いたときは実際そうなっていた。
RUN_PY = re.compile(
    r"""(?:^|[;&|`(]\s*|\s)(?:python[\d.]*|py)(?:\.exe)?\s+(?:-[^\s]+\s+)*["']?([^\s"']+\.py)"""
)
INLINE_PY = re.compile(r"(?:^|[;&|`(]\s*|\s)(?:python[\d.]*|py)(?:\.exe)?\s+(?:-[^\s]+\s+)*-(?:c|m)\b")
HAS_PY = re.compile(r"\.py\b")

# 上から順に、最初に当たったものを採用する。
# 先頭一致ではなく部分一致にしているのは、cd foo && git status を取り逃がさないため。
BASH_RULES = [
    ("git", re.compile(r"(?:^|[;&|]\s*|\s)git\s")),
    ("HTTP 確認（curl / wget）", re.compile(r"\b(?:curl|wget)\b")),
    ("npm / node", re.compile(r"\b(?:npm|npx|node|pnpm|yarn)\b")),
    ("ファイル閲覧・検索", re.compile(r"(?:^|[;&|]\s*)(?:ls|dir|cat|head|tail|grep|rg|find|wc|sed|awk|echo)\s")),
]

# git は回数より内訳に意味が出る。確認系なのか、書き込み系なのか。
GIT_SUB = re.compile(r"(?:^|[;&|]\s*|\s)git\s+(?:--\S+\s+)*([a-z][a-z\-]*)")


def default_log_dir() -> Path:
    return Path(os.path.expanduser("~")) / ".claude" / "projects"


def iter_tool_uses(paths, since=None, until=None):
    """jsonl から tool_use だけを取り出す。壊れた行は捨てて続行する。"""
    for path in paths:
        try:
            fh = io.open(path, encoding="utf-8", errors="replace")
        except OSError:
            continue
        with fh:
            for line in fh:
                if '"tool_use"' not in line:
                    continue
                try:
                    rec = json.loads(line)
                except Exception:
                    continue
                ts = rec.get("timestamp")
                day = ts[:10] if isinstance(ts, str) and ts[:3] == "202" else None
                if since and (day is None or day < since):
                    continue
                if until and (day is None or day > until):
                    continue
                for block in (rec.get("message") or {}).get("content") or []:
                    if isinstance(block, dict) and block.get("type") == "tool_use":
                        yield path, day, block


def classify_bash(command: str):
    """(分類ラベル, 実行された .py のリスト) を返す。"""
    hits = RUN_PY.findall(command)
    if hits:
        return "スクリプト実行（置いてある .py）", hits
    if INLINE_PY.search(command):
        return "その場書きの Python（-c / -m）", []
    for label, rx in BASH_RULES:
        if rx.search(command):
            return label, []
    if HAS_PY.search(command):
        return ".py が引数に出るだけ", []
    return "その他", []


def main() -> int:
    ap = argparse.ArgumentParser(description="Claude Code の操作ログを数える")
    ap.add_argument("--dir", default=None, help="ログ置き場（既定: ~/.claude/projects）")
    ap.add_argument("--days", type=int, default=0,
                    help="直近 N 日だけを対象にする（当日を含むので結果は実行のたびに変わる）")
    ap.add_argument("--since", default=None, help="開始日 YYYY-MM-DD（この日を含む）")
    ap.add_argument("--until", default=None, help="終了日 YYYY-MM-DD（この日を含む）。期間を閉じると再現できる")
    ap.add_argument("--top", type=int, default=12, help="上位いくつまで表示するか")
    ap.add_argument("--csv", default=None, help="集計結果を CSV に書き出す")
    a = ap.parse_args()

    root = Path(a.dir) if a.dir else default_log_dir()
    if not root.exists():
        print(f"[ERROR] ログ置き場がありません: {root}", file=sys.stderr)
        print("        --dir でパスを指定してください。", file=sys.stderr)
        return 1

    files = sorted(root.glob("*/*.jsonl")) or sorted(root.glob("*.jsonl"))
    if not files:
        print(f"[ERROR] .jsonl が1つも見つかりません: {root}", file=sys.stderr)
        return 1

    since, until = a.since, a.until
    if a.days > 0 and not since:
        since = (dt.date.today() - dt.timedelta(days=a.days)).isoformat()
    if until is None and since is None and a.days == 0:
        pass  # 全期間

    tools = collections.Counter()
    bash_kinds = collections.Counter()
    scripts = collections.Counter()
    git_subs = collections.Counter()
    mcp_srv = collections.Counter()
    mcp_tool = collections.Counter()
    mcp_days = collections.defaultdict(set)
    per_session = collections.Counter()
    days = set()
    total = 0

    for path, day, block in iter_tool_uses(files, since, until):
        total += 1
        name = block.get("name") or "?"
        tools[name] += 1
        per_session[path.name] += 1
        if day:
            days.add(day)
        if name.startswith("mcp__"):
            # mcp__<サーバー>__<ツール>
            parts = name.split("__")
            server = parts[1] if len(parts) > 1 else "?"
            mcp_srv[server] += 1
            mcp_tool[name[len("mcp__"):]] += 1
            if day:
                mcp_days[server].add(day)
        if name == "Bash":
            cmd = str((block.get("input") or {}).get("command", ""))
            label, hits = classify_bash(cmd)
            bash_kinds[label] += 1
            for hit in hits:
                scripts[os.path.basename(hit.replace("\\", "/"))] += 1
            sub = GIT_SUB.findall(cmd)
            if sub:
                git_subs[sub[0]] += 1

    if total == 0:
        print("[INFO] 対象期間に tool_use がありませんでした。--days を広げてください。")
        return 0

    span = f"{min(days)} 〜 {max(days)}" if days else "（日付なし）"
    closed = " [期間を閉じた集計＝再現できる]" if until else " [当日を含む＝実行ごとに変わる]"
    counts = sorted(per_session.values())
    print(f"期間            {span}{closed}")
    print(f"セッション数     {len(per_session):,}")
    print(f"ツール呼び出し   {total:,}")
    print(f"1セッションあたり 中央値 {statistics.median(counts):.0f} / 平均 "
          f"{statistics.mean(counts):.0f} / 最大 {max(counts):,}")

    print("\n【ツール別】")
    for name, cnt in tools.most_common(a.top):
        print(f"  {name:<18}{cnt:>7,}  {cnt / total * 100:5.1f}%")

    bash_total = tools.get("Bash", 0)
    if bash_total:
        print(f"\n【Bash {bash_total:,} 件の中身】")
        for name, cnt in bash_kinds.most_common():
            print(f"  {name:<30}{cnt:>7,}  {cnt / bash_total * 100:5.1f}%"
                  f"  (全体の {cnt / total * 100:.1f}%)")

    mcp_total = sum(mcp_srv.values())
    if mcp_total:
        once = sum(1 for c in mcp_tool.values() if c == 1)
        print("")
        print(f"【MCP 経由】 {mcp_total:,} 件 = 全体の {mcp_total / total * 100:.1f}%"
              f" / サーバー {len(mcp_srv)}種 / ツール {len(mcp_tool)}種"
              f" / 1回きり {once}種")
        print(f"  {'サーバー':<26}{'回数':>7}{'MCP内':>8}{'使った日':>9}")
        for name, cnt in mcp_srv.most_common():
            print(f"  {name:<26}{cnt:>7,}{cnt / mcp_total * 100:>7.0f}%{len(mcp_days[name]):>8}日")
        print("  ツール別:")
        for name, cnt in mcp_tool.most_common(a.top):
            print(f"    {name:<44}{cnt:>5,}")

    if git_subs:
        gv = sum(git_subs.values())
        print(f"\n【git {gv:,} 件のサブコマンド】")
        for name, cnt in git_subs.most_common(8):
            print(f"  git {name:<22}{cnt:>7,}  {cnt / gv * 100:5.1f}%")

    if scripts:
        sv = sum(scripts.values())
        top10 = sum(c for _, c in scripts.most_common(10))
        print(f"\n【実行された .py】 種類 {len(scripts):,} / 実行 {sv:,}"
              f" / 上位10本が {top10 / sv * 100:.0f}%")
        for name, cnt in scripts.most_common(a.top):
            print(f"  {name:<34}{cnt:>6,}")

    if a.csv:
        # Windows で余分な CR が入らないよう newline="" を付ける。
        with io.open(a.csv, "w", encoding="utf-8", newline="") as fh:
            w = csv.writer(fh)
            w.writerow(["section", "name", "count", "pct_of_total"])
            for name, cnt in tools.most_common():
                w.writerow(["tool", name, cnt, round(cnt / total * 100, 2)])
            for name, cnt in bash_kinds.most_common():
                w.writerow(["bash_kind", name, cnt, round(cnt / total * 100, 2)])
            for name, cnt in git_subs.most_common():
                w.writerow(["git_sub", name, cnt, round(cnt / total * 100, 2)])
            for name, cnt in mcp_srv.most_common():
                w.writerow(["mcp_server", name, cnt, round(cnt / total * 100, 2)])
            for name, cnt in mcp_tool.most_common():
                w.writerow(["mcp_tool", name, cnt, round(cnt / total * 100, 2)])
            for name, cnt in scripts.most_common():
                w.writerow(["script", name, cnt, round(cnt / total * 100, 2)])
        print(f"\n[OK] CSV: {a.csv}")

    return 0


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