#!/usr/bin/env python3
"""Fetch changelog for a Python package upgrade and summarize it with Claude.

Usage:
    python generate_changelog_summary.py <package> <current_version> <latest_version>

Prints a Markdown summary to stdout. Exits 0 with no output on any failure
(never blocks the calling workflow).

Environment variables:
    ANTHROPIC_API_KEY  - Required for LLM summarization
    GH_TOKEN           - Optional, used for GitHub Releases API (higher rate limits)
"""

import json
import os
import re
import sys
import urllib.request
from pathlib import Path

from packaging.version import Version

PYPI_URL = "https://pypi.org/pypi/{}/json"
GITHUB_RELEASES_URL = "https://api.github.com/repos/{}/{}/releases?per_page=100"
MAX_CHANGELOG_CHARS = 30_000

GITHUB_URL_KEYS = ["Source", "Repository", "Source Code", "GitHub", "Homepage", "Code"]
CHANGELOG_KEYS = ["Changelog", "Changes", "Release Notes", "History", "What's New"]


def fetch_pypi_urls(package: str) -> dict[str, str]:
    """Fetch project_urls from PyPI."""
    url = PYPI_URL.format(package)
    req = urllib.request.Request(url, headers={"Accept": "application/json"})
    with urllib.request.urlopen(req, timeout=15) as resp:
        data = json.loads(resp.read())
    return data.get("info", {}).get("project_urls") or {}


def _find_url(project_urls: dict[str, str], candidates: list[str]) -> str | None:
    """Case-insensitive, substring-based lookup in project_urls."""
    lower_urls = {k.lower(): v for k, v in project_urls.items()}
    for candidate in candidates:
        c = candidate.lower()
        # Exact match first
        if c in lower_urls:
            return lower_urls[c]
        # Substring match (e.g. "Docs: Changelog" contains "changelog")
        for key, url in lower_urls.items():
            if c in key:
                return url
    return None


def extract_github_repo(project_urls: dict[str, str]) -> tuple[str, str] | None:
    """Extract (owner, repo) from project_urls, or None."""
    # Try known keys first (case-insensitive)
    for candidate in GITHUB_URL_KEYS:
        url = _find_url(project_urls, [candidate]) or ""
        m = re.search(r"github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/|$)", url)
        if m:
            return m.group(1), m.group(2)
    # Try all URLs as fallback
    for url in project_urls.values():
        m = re.search(r"github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/|$)", url)
        if m:
            return m.group(1), m.group(2)
    return None


def extract_changelog_url(project_urls: dict[str, str]) -> str | None:
    """Extract a changelog URL from project_urls."""
    return _find_url(project_urls, CHANGELOG_KEYS)


def parse_tag_version(tag: str) -> Version | None:
    """Parse a version from a git tag, handling common prefixes."""
    tag = tag.strip()
    # Strip common prefixes
    for prefix in ["v", "V", "release-", "release/"]:
        if tag.startswith(prefix):
            tag = tag[len(prefix):]
            break
    try:
        return Version(tag)
    except Exception:
        return None


def fetch_github_releases(
    owner: str,
    repo: str,
    from_ver: Version,
    to_ver: Version,
) -> str:
    """Fetch GitHub release notes for versions in (from_ver, to_ver]."""
    url = GITHUB_RELEASES_URL.format(owner, repo)
    headers = {"Accept": "application/vnd.github+json"}
    gh_token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
    if gh_token:
        headers["Authorization"] = f"Bearer {gh_token}"

    req = urllib.request.Request(url, headers=headers)
    with urllib.request.urlopen(req, timeout=30) as resp:
        releases = json.loads(resp.read())

    relevant = []
    for rel in releases:
        tag = rel.get("tag_name", "")
        ver = parse_tag_version(tag)
        if ver is None:
            continue
        if from_ver < ver <= to_ver:
            body = rel.get("body") or ""
            name = rel.get("name") or tag
            relevant.append((ver, name, body))

    if not relevant:
        return ""

    relevant.sort(key=lambda x: x[0])
    parts = []
    for ver, name, body in relevant:
        parts.append(f"## {name}\n\n{body}")

    text = "\n\n---\n\n".join(parts)
    if len(text) > MAX_CHANGELOG_CHARS:
        text = text[:MAX_CHANGELOG_CHARS] + "\n\n[Changelog truncated — showing most recent entries]"
    return text


def fetch_raw_changelog(url: str) -> str:
    """Fetch raw changelog content from a URL."""
    req = urllib.request.Request(url, headers={"User-Agent": "earningscall-upgrade-bot"})
    with urllib.request.urlopen(req, timeout=15) as resp:
        content_type = resp.headers.get("Content-Type", "")
        text = resp.read().decode("utf-8", errors="replace")
    # If it's HTML, we still send it — Claude can parse it
    if len(text) > MAX_CHANGELOG_CHARS:
        text = text[:MAX_CHANGELOG_CHARS] + "\n\n[Changelog truncated]"
    return text


def summarize_with_claude(
    package: str,
    from_ver: str,
    to_ver: str,
    changelog_text: str,
) -> str:
    """Summarize changelog using Claude API."""
    import anthropic

    client = anthropic.Anthropic()
    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=(
            "You are a dependency upgrade analyst. Given raw changelog entries for a "
            "Python package upgrade, produce a concise summary for a PR reviewer. "
            "Use Markdown formatting."
        ),
        messages=[
            {
                "role": "user",
                "content": (
                    f"Package: **{package}**\n"
                    f"Upgrading from `{from_ver}` to `{to_ver}`\n\n"
                    f"Raw changelog:\n\n{changelog_text}\n\n"
                    "Instructions:\n"
                    "- Summarize the notable changes in 5-15 bullet points\n"
                    "- Highlight any BREAKING CHANGES or deprecations prominently at the top\n"
                    "- Note any security fixes\n"
                    "- Ignore trivial changes (CI, docs typos, internal refactors)\n"
                    "- Focus on changes that could impact downstream consumers\n"
                    "- Be concise — each bullet should be one sentence\n"
                ),
            }
        ],
    )
    return message.content[0].text


def main() -> None:
    if len(sys.argv) != 4:
        print(
            "Usage: generate_changelog_summary.py <package> <current_version> <latest_version>",
            file=sys.stderr,
        )
        sys.exit(1)

    package = sys.argv[1]
    current_version = sys.argv[2]
    latest_version = sys.argv[3]

    api_key = os.environ.get("ANTHROPIC_API_KEY")
    if not api_key:
        print("ANTHROPIC_API_KEY not set, skipping changelog summary", file=sys.stderr)
        return

    from_ver = Version(current_version)
    to_ver = Version(latest_version)

    # Step 1: Get project URLs from PyPI
    try:
        project_urls = fetch_pypi_urls(package)
    except Exception as e:
        print(f"Failed to fetch PyPI metadata: {e}", file=sys.stderr)
        return

    # Step 2: Try GitHub releases first, then raw changelog
    changelog_text = ""

    github_repo = extract_github_repo(project_urls)
    if github_repo:
        owner, repo = github_repo
        try:
            changelog_text = fetch_github_releases(owner, repo, from_ver, to_ver)
            if changelog_text:
                print(
                    f"Fetched GitHub releases for {owner}/{repo}",
                    file=sys.stderr,
                )
        except Exception as e:
            print(f"Failed to fetch GitHub releases: {e}", file=sys.stderr)

    if not changelog_text:
        changelog_url = extract_changelog_url(project_urls)
        if changelog_url:
            try:
                changelog_text = fetch_raw_changelog(changelog_url)
                if changelog_text:
                    print(f"Fetched changelog from {changelog_url}", file=sys.stderr)
            except Exception as e:
                print(f"Failed to fetch changelog: {e}", file=sys.stderr)

    if not changelog_text:
        print("No changelog information found", file=sys.stderr)
        return

    # Step 3: Summarize with Claude
    try:
        summary = summarize_with_claude(package, current_version, latest_version, changelog_text)
        print(summary)
    except Exception as e:
        print(f"Failed to summarize with Claude: {e}", file=sys.stderr)


if __name__ == "__main__":
    main()
