#!/usr/bin/env python3
"""
Generate a "what did I get done" Markdown report from git commits across repos.

Discovers git repos under a root path, collects commits by the current user in a
time range, optionally summarizes per-repo with OpenRouter, and writes a grouped
MD file.

Usage:
  # Last 24h, repos in parent dir (e.g. ~/Documents/GitHub), no AI
  python what_did_i_get_done.py --no-ai

  # Last 7 days, all repos under ~/Documents/GitHub, with OpenRouter summary
  export OPENROUTER_API_KEY=sk-...
  python what_did_i_get_done.py --root ~/Documents/GitHub --since "7 days ago"

  # Recursive discovery (e.g. org/repo), custom output path
  python what_did_i_get_done.py -r --since "1 week ago" -o report.md

  # Exclude specific repos
  python what_did_i_get_done.py --exclude some-repo --exclude other-repo
"""

from __future__ import annotations
import argparse
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import subprocess
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
DEFAULT_MODEL = "openai/gpt-4o-mini"


@dataclass
class Commit:
    """A single git commit."""

    hash: str
    date: str
    subject: str
    additions: int = 0
    deletions: int = 0
    timestamp: int = 0  # Unix timestamp for grouping by hour


@dataclass
class RepoResult:
    """Commits and optional AI summary for one repo."""

    repo_path: Path
    repo_name: str
    commits: list[Commit] = field(default_factory=list)
    summary: str | None = None


def find_git_repos(root: Path, recursive: bool = False) -> list[Path]:
    """Return list of directory paths that are git repo roots under root."""
    root = root.resolve()
    if not root.is_dir():
        return []
    repos: list[Path] = []
    if recursive:
        for path in root.rglob(".git"):
            if path.is_dir():
                repo_root = path.parent
                repos.append(repo_root)
        return sorted(set(repos))
    for path in root.iterdir():
        if path.is_dir() and (path / ".git").is_dir():
            repos.append(path)
    return sorted(repos)


def get_git_author_email(repo_path: Path) -> str | None:
    """Get configured user.email for the repo (or global)."""
    try:
        out = subprocess.run(
            ["git", "config", "user.email"],
            cwd=repo_path,
            capture_output=True,
            text=True,
            check=True,
            timeout=5,
        )
        return out.stdout.strip() or None
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
        return None


def get_commits(
    repo_path: Path,
    since: str,
    author_email: str | None,
) -> list[Commit]:
    """Return non-merge commits in repo since the given time, optionally by author."""
    cmd = [
        "git",
        "log",
        f"--since={since}",
        "--no-merges",
        "--numstat",
        "--format=COMMIT:%h|%ad|%at|%s",
        "--date=short",
    ]
    if author_email:
        cmd.extend(["--author", author_email])
    try:
        out = subprocess.run(
            cmd,
            cwd=repo_path,
            capture_output=True,
            text=True,
            check=True,
            timeout=60,
        )
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
        return []
    commits: list[Commit] = []
    current_hash: str | None = None
    current_date: str | None = None
    current_subject: str | None = None
    current_timestamp = 0
    current_additions = 0
    current_deletions = 0

    for line in out.stdout.splitlines():
        line = line.strip()
        if line.startswith("COMMIT:"):
            if current_hash is not None:
                commits.append(
                    Commit(
                        hash=current_hash,
                        date=current_date or "",
                        subject=current_subject or "",
                        additions=current_additions,
                        deletions=current_deletions,
                        timestamp=current_timestamp,
                    )
                )
            _, rest = line.split(":", 1)
            parts = rest.split("|", 3)
            current_hash = parts[0] if len(parts) > 0 else ""
            current_date = parts[1] if len(parts) > 1 else ""
            current_timestamp = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 0
            current_subject = parts[3] if len(parts) > 3 else ""
            current_additions = 0
            current_deletions = 0
        elif line and current_hash is not None:
            # numstat: additions\tdeletions\tpath (path can have tabs)
            parts = line.split("\t", 2)
            if len(parts) >= 2:
                try:
                    add = int(parts[0]) if parts[0] != "-" else 0
                    sub = int(parts[1]) if parts[1] != "-" else 0
                    current_additions += add
                    current_deletions += sub
                except ValueError:
                    pass

    if current_hash is not None:
        commits.append(
            Commit(
                hash=current_hash,
                date=current_date or "",
                subject=current_subject or "",
                additions=current_additions,
                deletions=current_deletions,
                timestamp=current_timestamp,
            )
        )
    return commits


def summarize_with_openrouter(
    repo_name: str,
    commits: list[Commit],
    api_key: str,
    model: str = DEFAULT_MODEL,
) -> str | None:
    """Call OpenRouter to get a 2–3 sentence summary of the commits. Returns None on error."""
    lines = [f"- {c.hash} {c.date} {c.subject}" for c in commits]
    prompt = f"""Summarize what was done in this repo in the given time window in 2–3 concise, information-dense sentences. Focus on substantial behavior or architecture changes. Do not list every commit.

Repo: {repo_name}

Commits:
{chr(10).join(lines)}

Summary:"""
    body = {
        "model": model,
        "messages": [{"role": "user", "content": prompt}],
    }
    req = Request(
        OPENROUTER_URL,
        data=json.dumps(body).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    try:
        with urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read().decode("utf-8"))
    except (HTTPError, URLError, TimeoutError, json.JSONDecodeError, KeyError):
        return None
    choices = data.get("choices")
    if not choices or not isinstance(choices, list):
        return None
    msg = choices[0].get("message")
    if not msg or not isinstance(msg, dict):
        return None
    content = msg.get("content")
    return content.strip() if isinstance(content, str) else None


# Distinct colors for up to ~15 repos in mermaid xychart
CHART_COLORS = [
    "#3b82f6", "#ef4444", "#22c55e", "#f59e0b", "#8b5cf6",
    "#ec4899", "#06b6d4", "#84cc16", "#f97316", "#6366f1",
    "#14b8a6", "#a855f7", "#eab308", "#0ea5e9", "#d946ef",
]


BUCKET_SECONDS = 900  # 15 minutes


def _build_mermaid_chart(results: list[RepoResult]) -> tuple[str, str]:
    """Build a mermaid xychart-beta with lines changed per 15 min per repo.
    Returns (frontmatter_for_colors, chart_markdown). Frontmatter is empty if no chart.
    """
    # (repo_name, bucket_ts) -> net lines
    by_repo_bucket: dict[str, dict[int, int]] = defaultdict(lambda: defaultdict(int))
    all_buckets: set[int] = set()

    for r in results:
        for c in r.commits:
            if c.timestamp:
                bucket_ts = (c.timestamp // BUCKET_SECONDS) * BUCKET_SECONDS
                net = c.additions - c.deletions
                by_repo_bucket[r.repo_name][bucket_ts] += net
                all_buckets.add(bucket_ts)

    if not all_buckets:
        return "", ""

    sorted_buckets = sorted(all_buckets)
    bucket_labels = [
        datetime.fromtimestamp(b, tz=timezone.utc).strftime("%m-%d %H:%M")
        for b in sorted_buckets
    ]
    max_val = 0
    for repo_data in by_repo_bucket.values():
        for v in repo_data.values():
            max_val = max(max_val, v)

    repo_names = sorted(by_repo_bucket.keys())
    palette = ", ".join(CHART_COLORS[: len(repo_names)])
    frontmatter = "\n".join([
        "---",
        "config:",
        "  themeVariables:",
        "    xyChart:",
        f'      plotColorPalette: "{palette}"',
        "  xyChart:",
        "    width: 2400",
        "    height: 500",
        "    xAxis:",
        "      labelFontSize: 6",
        "---",
        "",
    ])

    x_axis_str = json.dumps(bucket_labels)
    chart_lines = [
        "```mermaid",
        "xychart-beta",
        '    title "Lines changed per 15 min by repo"',
        f"    x-axis {x_axis_str}",
        f'    y-axis "Net lines" 0 --> {max(1, int(max_val * 1.1))}',
    ]
    for repo_name in repo_names:
        repo_data = by_repo_bucket[repo_name]
        values = [repo_data.get(b, 0) for b in sorted_buckets]
        label = f'"{repo_name}"' if any(c in repo_name for c in " -.") else repo_name
        values_str = json.dumps(values)
        chart_lines.append(f"    bar {label} {values_str}")
    chart_lines.append("```")

    return frontmatter, "\n".join(chart_lines)


def _fmt_diff(additions: int, deletions: int) -> str:
    """Format line count as +X -Y."""
    parts = []
    if additions:
        parts.append(f"+{additions}")
    if deletions:
        parts.append(f"-{deletions}")
    return " ".join(parts) if parts else ""


def render_md(
    results: list[RepoResult],
    since: str,
    date_range_human: str,
    use_ai_summary: bool,
) -> str:
    """Build the Markdown report."""
    frontmatter, chart = _build_mermaid_chart(results)

    lines = [
        "# What did I get done",
        "",
        f"**Time window:** {since}  ",
        f"**Date range:** {date_range_human}",
        "",
        "---",
        "",
    ]

    if chart:
        lines.append("## Activity by 15 min")
        lines.append("")
        lines.append(chart)
        lines.append("")
        lines.append("---")
        lines.append("")

    summary_rows: list[tuple[str, int, int, int]] = []  # (repo, commits, +lines, -lines)

    for r in results:
        if not r.commits:
            continue
        repo_additions = sum(c.additions for c in r.commits)
        repo_deletions = sum(c.deletions for c in r.commits)
        summary_rows.append((r.repo_name, len(r.commits), repo_additions, repo_deletions))

        diff_str = _fmt_diff(repo_additions, repo_deletions)
        header = f"## {r.repo_name}"
        if diff_str:
            header += f" ({diff_str})"
        lines.append(header)
        lines.append("")
        if use_ai_summary and r.summary:
            lines.append(r.summary)
            lines.append("")
        lines.append("**Commits:**")
        lines.append("")
        for c in r.commits:
            c_diff = _fmt_diff(c.additions, c.deletions)
            line = f"- `{c.hash}` {c.date} — {c.subject}"
            if c_diff:
                line += f" ({c_diff})"
            lines.append(line)
        lines.append("")

    # Summary table at bottom
    if summary_rows:
        total_commits = sum(row[1] for row in summary_rows)
        total_additions = sum(row[2] for row in summary_rows)
        total_deletions = sum(row[3] for row in summary_rows)
        lines.append("---")
        lines.append("")
        lines.append("## Summary")
        lines.append("")
        lines.append("| Repo | Commits | +Lines | -Lines | Net |")
        lines.append("|------|---------|--------|--------|-----|")
        for repo, commits, add, sub in summary_rows:
            net = add - sub
            net_str = f"+{net}" if net >= 0 else str(net)
            lines.append(f"| {repo} | {commits} | {add} | {sub} | {net_str} |")
        net_total = total_additions - total_deletions
        net_total_str = f"+{net_total}" if net_total >= 0 else str(net_total)
        lines.append(f"| **Total** | **{total_commits}** | **{total_additions}** | **{total_deletions}** | **{net_total_str}** |")
        lines.append("")

    body = "\n".join(lines)
    return (frontmatter + body) if frontmatter else body


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Generate a per-repo 'what did I get done' Markdown report from git commits."
    )
    parser.add_argument(
        "--root",
        type=Path,
        default=Path.home() / "Documents" / "GitHub",
        help="Root directory under which to discover git repos (default: ~/Documents/GitHub)",
    )
    parser.add_argument(
        "--since",
        default="24 hours ago",
        help="Git --since value (e.g. '24 hours ago', '7 days ago', '2025-01-01')",
    )
    parser.add_argument(
        "-o",
        "--output",
        type=Path,
        default=None,
        help="Output Markdown file (default: what_did_i_get_done_<date>.md in cwd)",
    )
    parser.add_argument(
        "--openrouter-key",
        default=os.environ.get("OPENROUTER_API_KEY", ""),
        help="OpenRouter API key for per-repo summaries (default: OPENROUTER_API_KEY env)",
    )
    parser.add_argument(
        "--openrouter-model",
        default=DEFAULT_MODEL,
        help=f"OpenRouter model (default: {DEFAULT_MODEL})",
    )
    parser.add_argument(
        "--no-ai",
        action="store_true",
        help="Skip AI summarization even if OpenROUTER_API_KEY is set",
    )
    parser.add_argument(
        "-r",
        "--recursive",
        action="store_true",
        help="Discover repos recursively under --root (e.g. org/repo)",
    )
    parser.add_argument(
        "--exclude",
        action="append",
        default=[],
        metavar="REPO",
        help="Exclude repo(s) by name (can be repeated, e.g. --exclude foo --exclude bar)",
    )
    args = parser.parse_args()

    root = args.root.resolve()
    if not root.is_dir():
        print(f"Error: root is not a directory: {root}", file=sys.stderr)
        return 1

    exclude_set = {name.strip() for name in args.exclude if name.strip()}
    repos = find_git_repos(root, recursive=args.recursive)
    repos = [r for r in repos if r.name not in exclude_set]
    if not repos:
        print("No git repos found under", root, file=sys.stderr)
        return 1

    use_ai = bool(args.openrouter_key.strip()) and not args.no_ai
    author_email = None
    results: list[RepoResult] = []

    for repo_path in repos:
        if author_email is None:
            author_email = get_git_author_email(repo_path)
        commits = get_commits(repo_path, args.since, author_email)
        repo_name = repo_path.name
        summary = None
        if use_ai and commits:
            summary = summarize_with_openrouter(
                repo_name, commits, args.openrouter_key.strip(), args.openrouter_model
            )
        results.append(
            RepoResult(repo_path=repo_path, repo_name=repo_name, commits=commits, summary=summary)
        )

    results_with_commits = [r for r in results if r.commits]
    if not results_with_commits:
        print("No commits in the given time window for any repo.", file=sys.stderr)
        return 0

    now = datetime.now(timezone.utc)
    date_range_human = now.strftime("%Y-%m-%d") + f" (since: {args.since})"
    md = render_md(results_with_commits, args.since, date_range_human, use_ai)

    out_path = args.output
    if out_path is None:
        out_path = Path.cwd() / f"what_did_i_get_done_{now.strftime('%Y-%m-%d_%H%M')}.md"
    out_path = out_path.resolve()
    out_path.write_text(md, encoding="utf-8")
    print(f"Wrote {len(results_with_commits)} repo(s) to {out_path}")
    return 0


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