#!/usr/bin/env python3
"""
Release script for padz.

Creates a new release by:
1. Validating the git repo is clean
2. Validating no existing git tag or crates.io version conflicts
3. Updating Cargo.toml with the new version
4. Committing the change
5. Creating and pushing a git tag

Usage:
    scripts/new-release --bump          # Bump patch version (0.9.0 -> 0.9.1)
    scripts/new-release 0.10.0          # Set explicit version
    scripts/new-release v0.10.0         # Also accepts 'v' prefix
"""

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


def run(cmd: list[str], capture: bool = True, check: bool = True) -> subprocess.CompletedProcess:
    """Run a command and return the result."""
    return subprocess.run(cmd, capture_output=capture, text=True, check=check)


def get_current_version(cargo_toml: Path) -> str:
    """Read the current version from Cargo.toml."""
    with open(cargo_toml, "rb") as f:
        data = tomllib.load(f)
    return data["package"]["version"]


def parse_version(version: str) -> tuple[int, int, int]:
    """Parse a semver string into (major, minor, patch)."""
    match = re.match(r"^v?(\d+)\.(\d+)\.(\d+)$", version)
    if not match:
        raise ValueError(f"Invalid version format: {version}")
    return int(match.group(1)), int(match.group(2)), int(match.group(3))


def version_tuple_to_str(v: tuple[int, int, int]) -> str:
    """Convert version tuple to string."""
    return f"{v[0]}.{v[1]}.{v[2]}"


def bump_patch(version: str) -> str:
    """Bump the patch part of a version."""
    major, minor, patch = parse_version(version)
    return version_tuple_to_str((major, minor, patch + 1))


def normalize_version(version: str) -> str:
    """Normalize version string (remove 'v' prefix if present)."""
    return version.lstrip("v")


def check_git_clean() -> None:
    """Ensure git repo is clean."""
    result = run(["git", "status", "--porcelain"])
    if result.stdout.strip():
        print("Error: Git repository is not clean.", file=sys.stderr)
        print("Please commit or stash your changes first.", file=sys.stderr)
        print("\nModified files:", file=sys.stderr)
        print(result.stdout, file=sys.stderr)
        sys.exit(1)


def ensure_latest(new_version: str, prior_versions: list[str]) -> str | None:
    """Check that new_version is greater than all prior versions.

    Args:
        new_version: The version we want to release
        prior_versions: List of version strings (with or without 'v' prefix)

    Returns:
        None if new_version is latest, otherwise the conflicting version string
    """
    new_v = parse_version(new_version)

    for ver_str in prior_versions:
        try:
            ver = parse_version(ver_str)
            if ver >= new_v:
                return version_tuple_to_str(ver)
        except ValueError:
            pass  # Skip malformed versions

    return None


def get_git_tags() -> list[str]:
    """Get all version tags from git as strings."""
    result = run(["git", "tag", "-l", "v*"])
    return [line for line in result.stdout.strip().split("\n") if line]


def check_git_tag_conflict(new_version: str) -> None:
    """Check that no git tag exists for this version or higher."""
    existing_tags = get_git_tags()
    conflict = ensure_latest(new_version, existing_tags)

    if conflict:
        print(f"Aborting: there's pre-existing git tag for v{conflict}.", file=sys.stderr)
        print("Remove or update Cargo.toml manually, then try again.", file=sys.stderr)
        sys.exit(1)


def get_crate_name(cargo_toml: Path) -> str:
    """Get the crate name from Cargo.toml."""
    with open(cargo_toml, "rb") as f:
        data = tomllib.load(f)
    return data["package"]["name"]


def get_crates_io_versions(crate_name: str) -> list[str]:
    """Get all versions of the crate from crates.io as strings."""
    # Use cargo search to check if crate exists
    result = run(["cargo", "search", crate_name, "--limit", "1"], check=False)
    if result.returncode != 0 or crate_name not in result.stdout:
        # Crate doesn't exist on crates.io yet
        return []

    # Use the crates.io API to get all versions
    # We'll use curl since it's available everywhere
    result = run(
        ["curl", "-s", f"https://crates.io/api/v1/crates/{crate_name}/versions"],
        check=False
    )
    if result.returncode != 0:
        return []

    # Parse the JSON response manually (no json module needed for simple extraction)
    # Look for "num":"X.Y.Z" patterns
    return [match.group(1) for match in re.finditer(r'"num"\s*:\s*"([^"]+)"', result.stdout)]


def check_crates_io_conflict(crate_name: str, new_version: str) -> None:
    """Check that no crates.io version exists for this version or higher."""
    existing_versions = get_crates_io_versions(crate_name)

    if not existing_versions:
        return  # Crate not on crates.io yet, or couldn't fetch

    conflict = ensure_latest(new_version, existing_versions)

    if conflict:
        print(f"Aborting: crates.io already has version {conflict} for {crate_name}.", file=sys.stderr)
        print("Remove or update Cargo.toml manually, then try again.", file=sys.stderr)
        sys.exit(1)


def update_cargo_toml(cargo_toml: Path, new_version: str) -> None:
    """Update the version in Cargo.toml."""
    content = cargo_toml.read_text()

    # Match version = "X.Y.Z" in the [package] section
    # We need to be careful to only replace the package version, not dependency versions
    # The package version comes early in the file, after [package]
    pattern = r'(\[package\][^\[]*?version\s*=\s*)"[^"]+"'
    replacement = rf'\1"{new_version}"'

    new_content, count = re.subn(pattern, replacement, content, count=1, flags=re.DOTALL)

    if count == 0:
        raise RuntimeError("Failed to find version in Cargo.toml")

    cargo_toml.write_text(new_content)


def git_commit_and_tag(new_version: str) -> None:
    """Commit the Cargo.toml change and create a tag."""
    tag = f"v{new_version}"

    # Stage Cargo.toml
    run(["git", "add", "Cargo.toml"])

    # Commit
    run(["git", "commit", "-m", f"Bumped version number to {tag}"])

    # Create tag
    run(["git", "tag", "-a", tag, "-m", f"Release {tag}"])

    # Push commit and tag
    run(["git", "push"])
    run(["git", "push", "origin", tag])


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Create a new release for padz.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s --bump          Bump patch version (0.9.0 -> 0.9.1)
  %(prog)s 0.10.0          Set explicit version
  %(prog)s v0.10.0         Also accepts 'v' prefix

The script will:
  1. Validate git repo is clean
  2. Check no conflicting git tags or crates.io versions exist
  3. Update Cargo.toml
  4. Commit, tag, and push
        """,
    )

    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument(
        "--bump",
        action="store_true",
        help="Bump the patch version (e.g., 0.9.0 -> 0.9.1)",
    )
    group.add_argument(
        "version",
        nargs="?",
        help="Explicit version to set (e.g., 0.10.0 or v0.10.0)",
    )

    args = parser.parse_args()

    # Find Cargo.toml
    cargo_toml = Path("Cargo.toml")
    if not cargo_toml.exists():
        print("Error: Cargo.toml not found. Run from project root.", file=sys.stderr)
        sys.exit(1)

    # Check git is clean first (before any other output)
    check_git_clean()

    # Get current version
    current_version = get_current_version(cargo_toml)
    print(f"Current version: {current_version}")

    # Determine new version
    if args.bump:
        new_version = bump_patch(current_version)
    else:
        new_version = normalize_version(args.version)
        # Validate format
        try:
            parse_version(new_version)
        except ValueError as e:
            print(f"Error: {e}", file=sys.stderr)
            sys.exit(1)

    print(f"New version: {new_version}")

    # Validations
    print("\nRunning validations...")

    print("  Checking git tags...", end=" ")
    check_git_tag_conflict(new_version)
    print("OK")

    crate_name = get_crate_name(cargo_toml)
    print(f"  Checking crates.io ({crate_name})...", end=" ")
    check_crates_io_conflict(crate_name, new_version)
    print("OK")

    # Update and release
    print(f"\nUpdating Cargo.toml to {new_version}...")
    update_cargo_toml(cargo_toml, new_version)

    print("Committing and tagging...")
    git_commit_and_tag(new_version)

    print(f"\n✅ Released v{new_version}")
    print(f"   Tag: v{new_version}")
    print("   CI will publish to crates.io")


if __name__ == "__main__":
    main()
