#!/usr/bin/env python3
"""
Plot lines of code (excluding comments) vs time for ui/, src/, and tests/.

Starts from December 2025. Generates:
- 4 plots (ui/, src/, tests/, total)
- A text file listing commits in decreasing order of added lines with log

Requires: matplotlib
"""

import subprocess
from datetime import datetime, date
from pathlib import Path

import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np


def count_python_lines(text: str) -> int:
    """Count non-comment, non-docstring, non-blank lines in Python source."""
    if not text:
        return 0
    lines = text.splitlines()
    count = 0
    in_docstring = False
    docstring_quote = None
    for line in lines:
        stripped = line.strip()
        if not stripped:
            continue
        if in_docstring:
            if docstring_quote and docstring_quote in stripped:
                end = stripped.find(docstring_quote) + len(docstring_quote)
                if end < len(stripped) and stripped[end:].strip():
                    count += 1  # code after closing quote
                in_docstring = False
            continue
        if stripped.startswith("#"):
            continue
        if stripped.startswith('"""') or stripped.startswith("'''"):
            q = '"""' if stripped.startswith('"""') else "'''"
            rest = stripped[len(q):]
            if q in rest:
                in_docstring = False
                if rest.split(q, 1)[1].strip():
                    count += 1  # code after """
            else:
                in_docstring = True
                docstring_quote = q
            continue
        # Inline comment: keep code before #
        if "#" in stripped:
            code_part = stripped.split("#")[0].strip()
            if code_part:
                count += 1
            continue
        count += 1
    return count


def count_lines_simple(text: str, ext: str) -> int:
    """Count non-comment lines for non-Python files (TOML, YAML, etc.)."""
    if not text:
        return 0
    lines = text.splitlines()
    count = 0
    for line in lines:
        s = line.strip()
        if not s:
            continue
        if s.startswith("#"):
            continue
        if ext in (".toml", ".yml", ".yaml", ".cfg", ".ini"):
            if s.startswith(";"):
                continue
        count += 1
    return count


def count_lines_in_content(content: str, path: str) -> int:
    """Count code lines (excluding comments) for a file."""
    ext = Path(path).suffix.lower()
    if ext == ".py":
        return count_python_lines(content)
    return count_lines_simple(content, ext)


def get_files_at_commit(repo_root: Path, commit: str, dirs: list[str]) -> dict[str, list[str]]:
    """List file paths in given dirs at a given commit."""
    result: dict[str, list[str]] = {d: [] for d in dirs}
    for d in dirs:
        try:
            out = subprocess.run(
                ["git", "ls-tree", "-r", "--name-only", commit, "--", d],
                cwd=repo_root,
                capture_output=True,
                text=True,
                check=True,
            )
            paths = [p for p in out.stdout.strip().splitlines() if p]
            code_ext = {".py", ".toml", ".yml", ".yaml"}
            result[d] = [p for p in paths if Path(p).suffix.lower() in code_ext]
        except subprocess.CalledProcessError:
            result[d] = []
    return result


def get_file_content_at_commit(repo_root: Path, commit: str, path: str) -> str:
    """Get file content at a given commit."""
    try:
        out = subprocess.run(
            ["git", "show", f"{commit}:{path}"],
            cwd=repo_root,
            capture_output=True,
            text=True,
        )
        if out.returncode != 0:
            return ""
        return out.stdout
    except Exception:
        return ""


def count_loc_at_commit(repo_root: Path, commit: str, dirs: list[str]) -> dict[str, int]:
    """Count lines of code in dirs at commit."""
    files = get_files_at_commit(repo_root, commit, dirs)
    counts: dict[str, int] = {d: 0 for d in dirs}
    for d, paths in files.items():
        for path in paths:
            content = get_file_content_at_commit(repo_root, commit, path)
            counts[d] += count_lines_in_content(content, path)
    return counts


def get_commits_since(repo_root: Path, since: date) -> list[tuple[str, datetime]]:
    """Get (commit_hash, date) for commits since given date."""
    since_str = since.strftime("%Y-%m-%d")
    out = subprocess.run(
        ["git", "log", "--format=%H %ci", f"--since={since_str}", "--reverse"],
        cwd=repo_root,
        capture_output=True,
        text=True,
        check=True,
    )
    result = []
    for line in out.stdout.strip().splitlines():
        if not line:
            continue
        parts = line.split()
        if len(parts) >= 2:
            h = parts[0]
            dt_str = " ".join(parts[1:4])  # "2025-08-07 07:58:03 -0400"
            try:
                dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S %z")
                result.append((h, dt.replace(tzinfo=None)))
            except ValueError:
                pass
    return result


def _is_code_line(line: str, ext: str) -> bool:
    """Return True if line counts as code (not comment, blank, or docstring)."""
    s = line.strip()
    if not s:
        return False
    if s.startswith("#"):
        return False
    if ext == ".py":
        if s.startswith('"""') or s.startswith("'''"):
            return False
    elif ext in (".toml", ".yml", ".yaml"):
        if s.startswith(";"):
            return False
    return True


def _dir_for_path(path: str) -> str | None:
    """Return 'ui', 'src', or 'tests' if path is under that dir, else None."""
    p = path.replace("\\", "/")
    if p.startswith("ui/"):
        return "ui"
    if p.startswith("src/"):
        return "src"
    if p.startswith("tests/"):
        return "tests"
    return None


def _code_lines_added_in_diff(diff_output: str) -> dict[str, int]:
    """Parse git diff -p output and count code lines added per dir (ui/src/tests)."""
    code_ext = {".py", ".toml", ".yml", ".yaml"}
    counts: dict[str, int] = {"ui": 0, "src": 0, "tests": 0}
    current_path = ""
    current_ext = ""
    for line in diff_output.split("\n"):
        if line.startswith("diff --git "):
            current_path = ""
            current_ext = ""
            continue
        if line.startswith("+++ b/"):
            current_path = line[6:].strip()
            current_ext = Path(current_path).suffix.lower()
            continue
        if line.startswith("+") and not line.startswith("++"):
            content = line[1:]
            if current_path and current_ext in code_ext:
                d = _dir_for_path(current_path)
                if d and _is_code_line(content, current_ext):
                    counts[d] += 1
    return counts


def generate_commits_by_lines_added(
    repo_root: Path, since: date, base_path: Path
) -> None:
    """Write 4 files: commits sorted by code lines added for ui, src, tests, total."""
    since_str = since.strftime("%Y-%m-%d")
    list_out = subprocess.run(
        ["git", "log", "--format=%H", f"--since={since_str}"],
        cwd=repo_root,
        capture_output=True,
        text=True,
        check=True,
    )
    hashes = [h.strip() for h in list_out.stdout.strip().split("\n") if h.strip()]
    commits: list[tuple[int, int, int, int, str, str, str, str]] = []
    for i, h in enumerate(hashes):
        if (i + 1) % 50 == 0 or i == 0:
            print(f"  Processing commit {i + 1}/{len(hashes)} for code diff...")
        out = subprocess.run(
            ["git", "show", "--format=%H%n%ai%n%s%n%b", "--no-patch", "-s", h],
            cwd=repo_root,
            capture_output=True,
            text=True,
        )
        meta = out.stdout.strip()
        diff_out = subprocess.run(
            ["git", "show", h, "-p", "--", "ui/", "src/", "tests/"],
            cwd=repo_root,
            capture_output=True,
            text=True,
        )
        if diff_out.returncode != 0:
            diff_text = ""
        else:
            idx = diff_out.stdout.find("diff --git ")
            diff_text = diff_out.stdout[idx:] if idx >= 0 else ""
        meta_lines = meta.split("\n")
        if len(meta_lines) < 3:
            continue
        dt = meta_lines[1].strip()
        subj = meta_lines[2].strip()
        body = "\n".join(meta_lines[3:]).strip() if len(meta_lines) > 3 else ""
        added = _code_lines_added_in_diff(diff_text)
        ui_a, src_a, tests_a = added["ui"], added["src"], added["tests"]
        total_a = ui_a + src_a + tests_a
        commits.append((ui_a, src_a, tests_a, total_a, h, dt, subj, body))

    dirs_order = ["ui", "src", "tests", "total"]
    for idx, dir_name in enumerate(dirs_order):
        sorted_commits = sorted(
            commits,
            key=lambda c: (c[idx], c[4]),
            reverse=True,
        )
        out_path = base_path / f"commits_by_lines_added_{dir_name}.txt"
        with open(out_path, "w") as f:
            f.write(
                f"Commits since December 2025, sorted by code lines added in {dir_name}/ (descending)\n"
            )
            f.write("(Comments and docstrings excluded, matching graph)\n")
            f.write("=" * 70 + "\n\n")
            for c in sorted_commits:
                ui_a, src_a, tests_a, total_a, h, dt, subj, body = c
                added = c[idx]
                f.write(f"+{added} code lines  {h[:12]}  {dt}\n")
                f.write(f"  (ui:{ui_a} src:{src_a} tests:{tests_a} total:{total_a})\n")
                f.write(f"  {subj}\n")
                if body.strip():
                    for bline in body.strip().split("\n"):
                        f.write(f"  {bline}\n")
                f.write("\n")
        print(f"Saved: {out_path}")


def sample_commits(commits: list[tuple[str, datetime]], max_points: int = 80) -> list[tuple[str, datetime]]:
    """Reduce to ~max_points by picking evenly spaced commits (keeps first and last)."""
    if len(commits) <= max_points:
        return commits
    indices = np.linspace(0, len(commits) - 1, max_points, dtype=int)
    return [commits[i] for i in indices]


def main():
    repo_root = Path(__file__).resolve().parent.parent
    dirs = ["ui", "src", "tests"]
    since = date(2025, 12, 1)

    commits = get_commits_since(repo_root, since)
    if not commits:
        print("No commits found since December 2025.")
        return

    commits = sample_commits(commits)

    dates: list[datetime] = []
    ui_counts: list[int] = []
    src_counts: list[int] = []
    tests_counts: list[int] = []

    for i, (commit, dt) in enumerate(commits):
        counts = count_loc_at_commit(repo_root, commit, dirs)
        dates.append(dt)
        ui_counts.append(counts["ui"])
        src_counts.append(counts["src"])
        tests_counts.append(counts["tests"])
        if (i + 1) % 20 == 0 or i == 0:
            print(f"  Processed {i + 1}/{len(commits)} commits...")

    total_counts = [u + s + t for u, s, t in zip(ui_counts, src_counts, tests_counts)]

    fig, axes = plt.subplots(2, 2, figsize=(12, 10), sharex=True)
    fig.suptitle("Lines of Code (excluding comments) vs Time\nFrom December 2025", fontsize=14)

    for ax, title, data in [
        (axes[0, 0], "ui/", ui_counts),
        (axes[0, 1], "src/", src_counts),
        (axes[1, 0], "tests/", tests_counts),
        (axes[1, 1], "Total (ui + src + tests)", total_counts),
    ]:
        ax.plot(dates, data, "b-", linewidth=1.5)
        ax.fill_between(dates, data, alpha=0.3)
        ax.set_ylabel("Lines of code")
        ax.set_title(title)
        ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
        ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
        plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha="right")
        ax.grid(True, alpha=0.3)

    plt.tight_layout()
    out_path = repo_root / "loc_over_time.png"
    plt.savefig(out_path, dpi=150, bbox_inches="tight")
    print(f"\nSaved: {out_path}")

    generate_commits_by_lines_added(repo_root, since, repo_root)


if __name__ == "__main__":
    main()
