#!/usr/bin/env python3
"""Changelog Update — generate or update CHANGELOG.md from git tags.

Offline, deterministic. No network calls.

Modes:
  --bootstrap     Generate initial CHANGELOG from existing tags
  --next VERSION  Add a new ``## [Unreleased]`` or ``## [VERSION]`` section

Reads: git tags (sorted by version), git log between tags
Writes: CHANGELOG.md (in repo root or specified ``--repo-root``)

Exit codes:
    0  Success
    1  Error (e.g. no tags found in bootstrap mode)
"""
from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path


def _run_git(args: list[str], cwd: Path) -> str:
    """Run a git command and return stdout, or empty string on failure."""
    try:
        result = subprocess.run(
            ["git", *args],
            cwd=str(cwd),
            capture_output=True,
            text=True,
            timeout=30,
        )
        return result.stdout.strip() if result.returncode == 0 else ""
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return ""


def _parse_version(tag: str) -> tuple[int, ...]:
    """Parse a version tag like v0.1.2 into a sortable tuple."""
    cleaned = tag.lstrip("v")
    parts = re.split(r"[.\-]", cleaned)
    result = []
    for p in parts:
        try:
            result.append(int(p))
        except ValueError:
            break
    return tuple(result) if result else (0,)


def get_tags(repo_path: Path) -> list[str]:
    """Return git tags sorted by semver (ascending)."""
    raw = _run_git(["tag", "-l"], repo_path)
    if not raw:
        return []
    tags = [t.strip() for t in raw.splitlines() if t.strip()]
    semver = [t for t in tags if re.match(r"^v?\d+\.\d+", t)]
    return sorted(semver, key=_parse_version)


def get_tag_date(repo_path: Path, tag: str) -> str:
    """Return YYYY-MM-DD date of the tag commit."""
    date = _run_git(["log", "-1", "--format=%as", tag], repo_path)
    return date if date else "unknown"


def get_commits_between(repo_path: Path, from_ref: str, to_ref: str) -> list[str]:
    """Return one-line commit messages between two refs."""
    raw = _run_git(
        ["log", "--oneline", "--no-merges", f"{from_ref}..{to_ref}"],
        repo_path,
    )
    if not raw:
        return []
    return [line.split(" ", 1)[1] if " " in line else line for line in raw.splitlines()]


def format_section(version: str, date: str, commits: list[str]) -> str:
    """Format a single changelog version section."""
    lines = [f"## [{version}] - {date}", ""]
    if commits:
        lines.append("### Changed")
        for msg in commits:
            lines.append(f"- {msg}")
    else:
        lines.append("### Added")
        lines.append("- Initial release")
    lines.append("")
    return "\n".join(lines)


def build_link_refs(repo_name: str, tags: list[str], github_org: str = "AbilityBI") -> str:
    """Build comparison link references for the bottom of the changelog."""
    lines = []
    for i, tag in enumerate(tags):
        version = tag.lstrip("v")
        if i == 0:
            lines.append(
                f"[{version}]: https://github.com/{github_org}/{repo_name}/releases/tag/{tag}"
            )
        else:
            prev = tags[i - 1]
            lines.append(
                f"[{version}]: https://github.com/{github_org}/{repo_name}/compare/{prev}...{tag}"
            )
    return "\n".join(lines)


def bootstrap(repo_path: Path, repo_name: str) -> str:
    """Generate a full CHANGELOG.md from existing git tags."""
    tags = get_tags(repo_path)
    if not tags:
        return ""

    header = f"""# Changelog

All notable changes to {repo_name} are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

"""
    sections = []
    for i, tag in enumerate(reversed(tags)):
        version = tag.lstrip("v")
        date = get_tag_date(repo_path, tag)
        if i < len(tags) - 1:
            prev_tag = tags[len(tags) - 2 - i]
            commits = get_commits_between(repo_path, prev_tag, tag)
        else:
            commits = []
        sections.append(format_section(version, date, commits))

    link_refs = build_link_refs(repo_name, tags)
    return header + "\n".join(sections) + link_refs + "\n"


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repo-root", default=".", metavar="PATH")
    parser.add_argument("--repo-name", default="", metavar="NAME")
    parser.add_argument("--bootstrap", action="store_true")
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args(argv)

    repo_path = Path(args.repo_root).resolve()
    repo_name = args.repo_name or repo_path.name

    if args.bootstrap:
        content = bootstrap(repo_path, repo_name)
        if not content:
            print(f"Error: no tags found in {repo_path}", file=sys.stderr)
            return 1
        if args.dry_run:
            print(content)
        else:
            out = repo_path / "CHANGELOG.md"
            out.write_text(content, encoding="utf-8")
            print(f"Wrote {out}")
        return 0

    parser.print_help()
    return 0


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