#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""
Apache Burr Release Script (SIMPLIFIED VERSION)

This script automates the Apache release process:
1. Create git archive (voting artifact)
2. Build source distribution (sdist)
3. Build wheel
4. Upload to Apache SVN

Usage:
    python scripts/apache_release_simplified.py all 0.41.0 0 myid
    python scripts/apache_release_simplified.py wheel 0.41.0 0
"""

import argparse
import glob
import hashlib
import os
import re
import shutil
import subprocess
import sys
from typing import NoReturn, Optional

# --- Configuration ---
PROJECT_SHORT_NAME = "burr"
VERSION_FILE = "pyproject.toml"
VERSION_PATTERN = r'version\s*=\s*"(\d+\.\d+\.\d+)"'

# Required examples for wheel (from pyproject.toml)
REQUIRED_EXAMPLES = [
    "__init__.py",
    "email-assistant",
    "multi-modal-chatbot",
    "streaming-fastapi",
    "deep-researcher",
    "hello-world-counter",
]


# ============================================================================
# Utility Functions
# ============================================================================


def _fail(message: str) -> NoReturn:
    """Print error message and exit."""
    print(f"\n❌ {message}")
    sys.exit(1)


def _print_section(title: str) -> None:
    """Print a formatted section header."""
    print("\n" + "=" * 80)
    print(f"  {title}")
    print("=" * 80 + "\n")


def _print_step(step_num: int, total: int, description: str) -> None:
    """Print a formatted step header."""
    print(f"\n[Step {step_num}/{total}] {description}")
    print("-" * 80)


def _run_command(
    cmd: list[str],
    description: str,
    error_message: str,
    success_message: Optional[str] = None,
    capture_output: bool = True,
    **kwargs,
) -> subprocess.CompletedProcess:
    """Run a subprocess command with consistent error handling and output.

    Args:
        cmd: Command and arguments as list
        description: What we're doing (printed before running)
        error_message: Error message prefix if command fails
        success_message: Optional success message (printed after if provided)
        capture_output: Whether to capture stdout/stderr (default True)
        **kwargs: Additional arguments to pass to subprocess.run

    Returns:
        CompletedProcess instance
    """
    if description:
        print(f"  {description}")

    try:
        result = subprocess.run(
            cmd,
            check=True,
            capture_output=capture_output,
            text=True,
            **kwargs,
        )
        if success_message:
            print(f"    ✓ {success_message}")
        return result
    except subprocess.CalledProcessError as e:
        error_detail = f": {e.stderr}" if capture_output and e.stderr else ""
        _fail(f"{error_message}{error_detail}")


# ============================================================================
# Environment Validation
# ============================================================================


def _validate_environment_for_command(args) -> None:
    """Validate required tools for the requested command."""
    print("\n" + "=" * 80)
    print("  Environment Validation")
    print("=" * 80 + "\n")

    # Define required tools for each command
    command_requirements = {
        "archive": ["git", "gpg"],
        "sdist": ["git", "gpg", "flit"],
        "wheel": ["git", "gpg", "flit", "node", "npm", "twine"],
        "upload": ["git", "gpg", "svn"],
        "all": ["git", "gpg", "flit", "node", "npm", "svn", "twine"],
        "verify": ["git", "gpg", "twine"],
    }

    required_tools = list(command_requirements.get(args.command, ["git", "gpg"]))

    # Drop gpg if user opted out of signing
    if getattr(args, "skip_signing", False) and "gpg" in required_tools:
        required_tools.remove("gpg")

    # Drop svn if user opted out of upload (svn is only used for upload)
    if getattr(args, "no_upload", False) and "svn" in required_tools:
        required_tools.remove("svn")

    # Check for RAT if needed
    if hasattr(args, "check_licenses") or hasattr(args, "check_licenses_report"):
        if getattr(args, "check_licenses", False) or getattr(args, "check_licenses_report", False):
            required_tools.append("java")
            if not getattr(args, "rat_jar", None):
                _fail("--rat-jar is required when using --check-licenses")

    # Check each tool
    missing_tools = []
    print("Checking required tools:")

    for tool in required_tools:
        if shutil.which(tool) is None:
            missing_tools.append(tool)
            print(f"  ✗ '{tool}' not found")
        else:
            print(f"  ✓ '{tool}' found")

    if missing_tools:
        print("\n❌ Missing required tools:")
        for tool in missing_tools:
            if tool == "flit":
                print(f"  • {tool}: Install with 'pip install flit'")
            elif tool == "twine":
                print(f"  • {tool}: Install with 'pip install twine'")
            elif tool in ["node", "npm"]:
                print(f"  • {tool}: Install from https://nodejs.org/")
            else:
                print(f"  • {tool}")
        sys.exit(1)

    print("\n✓ All required tools are available\n")


# ============================================================================
# Prerequisites
# ============================================================================


def _verify_project_root() -> bool:
    """Verify script is running from project root."""
    if not os.path.exists("pyproject.toml"):
        _fail("pyproject.toml not found. Please run from project root.")
    return True


def _get_version_from_file(file_path: str) -> str:
    """Extract version from pyproject.toml."""
    with open(file_path, encoding="utf-8") as f:
        content = f.read()
    match = re.search(VERSION_PATTERN, content)
    if match:
        return match.group(1)
    _fail(f"Could not find version in {file_path}")


def _validate_version(requested_version: str) -> bool:
    """Validate that requested version matches pyproject.toml."""
    current_version = _get_version_from_file(VERSION_FILE)
    if current_version != requested_version:
        _fail(
            f"Version mismatch!\n"
            f"  Requested: {requested_version}\n"
            f"  In {VERSION_FILE}: {current_version}\n"
            f"Please update {VERSION_FILE} to {requested_version} first."
        )
    print(f"✓ Version validated: {requested_version}\n")
    return True


def _check_git_working_tree() -> None:
    """Check git working tree status and warn if dirty."""
    try:
        dirty = (
            subprocess.check_output(["git", "status", "--porcelain"], stderr=subprocess.DEVNULL)
            .decode()
            .strip()
        )
        if dirty:
            print("⚠️  Warning: Git working tree has uncommitted changes:")
            for line in dirty.splitlines()[:10]:
                print(f"     {line}")
            if len(dirty.splitlines()) > 10:
                print(f"     ... and {len(dirty.splitlines()) - 10} more files")
            print()
    except subprocess.CalledProcessError:
        pass


# ============================================================================
# Signing and Verification
# ============================================================================


def _checksum_artifact(artifact_path: str) -> str:
    """Create SHA512 checksum for artifact."""
    checksum_path = f"{artifact_path}.sha512"
    sha512_hash = hashlib.sha512()
    with open(artifact_path, "rb") as f:
        while chunk := f.read(65536):
            sha512_hash.update(chunk)
    with open(checksum_path, "w", encoding="utf-8") as f:
        f.write(f"{sha512_hash.hexdigest()}\n")
    print(f"  ✓ Created SHA512 checksum: {checksum_path}")
    return checksum_path


def _sign_artifact(artifact_path: str, skip_signing: bool = False) -> tuple[Optional[str], str]:
    """Sign artifact with GPG (unless skipped) and create SHA512 checksum."""
    signature_path: Optional[str] = None
    if not skip_signing:
        signature_path = f"{artifact_path}.asc"
        _run_command(
            ["gpg", "--armor", "--output", signature_path, "--detach-sig", artifact_path],
            description="",
            error_message="Error signing artifact",
            capture_output=False,
        )
        print(f"  ✓ Created GPG signature: {signature_path}")
    else:
        print("  ⊘ Skipping GPG signature (--skip-signing)")

    checksum_path = _checksum_artifact(artifact_path)
    return (signature_path, checksum_path)


def _verify_artifact_signature(artifact_path: str, signature_path: str) -> bool:
    """Verify GPG signature of artifact."""
    if not os.path.exists(signature_path):
        print(f"    ✗ Signature file not found: {signature_path}")
        return False

    try:
        result = subprocess.run(
            ["gpg", "--verify", signature_path, artifact_path],
            capture_output=True,
            check=False,
        )
        if result.returncode == 0:
            print("    ✓ GPG signature is valid")
            return True
        else:
            print("    ✗ GPG signature verification failed")
            return False
    except subprocess.CalledProcessError:
        return False


def _verify_artifact_checksum(artifact_path: str, checksum_path: str) -> bool:
    """Verify SHA512 checksum of artifact."""
    if not os.path.exists(checksum_path):
        print(f"    ✗ Checksum file not found: {checksum_path}")
        return False

    # Read expected checksum
    with open(checksum_path, "r", encoding="utf-8") as f:
        expected_checksum = f.read().strip().split()[0]

    # Calculate actual checksum
    sha512_hash = hashlib.sha512()
    with open(artifact_path, "rb") as f:
        while chunk := f.read(65536):
            sha512_hash.update(chunk)

    actual_checksum = sha512_hash.hexdigest()

    if actual_checksum == expected_checksum:
        print("    ✓ SHA512 checksum is valid")
        return True
    else:
        print("    ✗ SHA512 checksum mismatch!")
        return False


def _verify_artifact_complete(artifact_path: str, skip_signing: bool = False) -> bool:
    """Verify artifact and its signature/checksum files."""
    print(f"\nVerifying artifact: {os.path.basename(artifact_path)}")

    if not os.path.exists(artifact_path):
        print(f"    ✗ Artifact not found: {artifact_path}")
        return False

    # Verify signature (unless skipped) and checksum
    checksum_path = f"{artifact_path}.sha512"
    checksum_valid = _verify_artifact_checksum(artifact_path, checksum_path)

    if skip_signing:
        if checksum_valid:
            print(f"  ✓ Checks passed for {os.path.basename(artifact_path)} (signing skipped)\n")
            return True
        return False

    signature_path = f"{artifact_path}.asc"
    sig_valid = _verify_artifact_signature(artifact_path, signature_path)

    if sig_valid and checksum_valid:
        print(f"  ✓ All checks passed for {os.path.basename(artifact_path)}\n")
        return True
    return False


# ============================================================================
# Step 1: Git Archive
# ============================================================================


def _create_git_archive(
    version: str, rc_num: str, output_dir: str = "dist", skip_signing: bool = False
) -> str:
    """Create git archive tar.gz for voting."""
    print(f"Creating git archive for version {version}-incubating...")

    os.makedirs(output_dir, exist_ok=True)

    archive_name = f"apache-burr-{version}-incubating-src.tar.gz"
    archive_path = os.path.join(output_dir, archive_name)
    prefix = f"apache-burr-{version}-incubating-src/"

    _run_command(
        [
            "git",
            "archive",
            "HEAD",
            f"--prefix={prefix}",
            "--format=tar.gz",
            "--output",
            archive_path,
        ],
        description="",
        error_message="Error creating git archive",
        capture_output=False,
    )
    print(f"  ✓ Created git archive: {archive_path}")

    file_size = os.path.getsize(archive_path)
    print(f"  ✓ Archive size: {file_size:,} bytes")

    print("Signing archive..." if not skip_signing else "Creating checksum for archive...")
    _sign_artifact(archive_path, skip_signing=skip_signing)

    # Verify
    if not _verify_artifact_complete(archive_path, skip_signing=skip_signing):
        _fail("Archive verification failed!")

    return archive_path


# ============================================================================
# Step 2: Build Source Distribution (sdist)
# ============================================================================


def _remove_ui_build_artifacts() -> None:
    """Remove pre-built UI artifacts to ensure clean build."""
    ui_build_dir = os.path.join("burr", "tracking", "server", "build")
    if os.path.exists(ui_build_dir):
        print(f"  Removing UI build artifacts: {ui_build_dir}")
        shutil.rmtree(ui_build_dir)
        print("    ✓ UI build artifacts removed")


def _build_sdist_from_git(version: str, output_dir: str = "dist") -> str:
    """Build source distribution from git using flit."""
    _print_step(1, 2, "Building sdist with flit")

    os.makedirs(output_dir, exist_ok=True)
    _remove_ui_build_artifacts()
    _check_git_working_tree()

    env = os.environ.copy()
    env["FLIT_USE_VCS"] = "0"
    _run_command(
        ["flit", "build", "--format", "sdist"],
        description="Running flit build --format sdist...",
        error_message="Failed to build sdist",
        success_message="flit sdist created successfully",
        env=env,
    )

    # Find and rename sdist
    expected_pattern = f"dist/apache_burr-{version.lower()}.tar.gz"
    sdist_files = glob.glob(expected_pattern)

    if not sdist_files:
        _fail(f"Could not find sdist: {expected_pattern}")

    original_sdist = sdist_files[0]
    apache_sdist = os.path.join(
        output_dir, f"apache-burr-{version.lower()}-incubating-sdist.tar.gz"
    )

    if os.path.exists(apache_sdist):
        os.remove(apache_sdist)

    shutil.move(original_sdist, apache_sdist)
    print(f"    ✓ Renamed to: {os.path.basename(apache_sdist)}")

    return apache_sdist


# ============================================================================
# Step 3: Build Wheel (SIMPLIFIED!)
# ============================================================================


def _build_ui_artifacts() -> None:
    """Build UI artifacts (npm build + copy to burr/tracking/server/build).

    This replicates the logic from burr.cli.__main__.run_build_ui_bash_commands()
    without requiring burr to be installed.
    """
    print("Building UI artifacts...")

    ui_source_dir = "telemetry/ui"
    ui_build_dir = "burr/tracking/server/build"

    # Clean existing UI build
    if os.path.exists(ui_build_dir):
        shutil.rmtree(ui_build_dir)

    # Install npm dependencies
    _run_command(
        ["npm", "install", "--prefix", ui_source_dir],
        description="Installing npm dependencies...",
        error_message="npm install failed",
        success_message="npm dependencies installed",
    )

    # Build UI with npm
    _run_command(
        ["npm", "run", "build", "--prefix", ui_source_dir],
        description="Building UI with npm...",
        error_message="npm build failed",
        success_message="npm build completed",
    )

    # Copy build artifacts
    print("  Copying build artifacts...")
    os.makedirs(ui_build_dir, exist_ok=True)
    ui_output = os.path.join(ui_source_dir, "build")

    try:
        shutil.copytree(ui_output, ui_build_dir, dirs_exist_ok=True)
        print("    ✓ Build artifacts copied")
    except Exception as e:
        _fail(f"Failed to copy build artifacts: {e}")

    # Verify
    if not os.path.exists(ui_build_dir) or not os.listdir(ui_build_dir):
        _fail(f"UI build directory is empty: {ui_build_dir}")


def _prepare_wheel_contents() -> tuple[bool, bool, Optional[str]]:
    """Handle burr/examples symlink: replace with real files for wheel."""
    burr_examples_dir = "burr/examples"
    source_examples_dir = "examples"

    if not os.path.exists(source_examples_dir):
        print(f"    ⚠️  {source_examples_dir} not found")
        return (False, False, None)

    # Check if burr/examples is a symlink (should be in dev repo)
    was_symlink = False
    symlink_target = None

    # Use lexists (not exists) so we detect broken symlinks too — CI runners
    # sometimes check out burr/examples as a symlink whose relative target
    # doesn't resolve from the working directory, and os.path.exists would
    # return False for such a link while os.makedirs would still blow up on it.
    if os.path.lexists(burr_examples_dir):
        if os.path.islink(burr_examples_dir):
            was_symlink = True
            symlink_target = os.readlink(burr_examples_dir)
            print(f"  Removing symlink: burr/examples -> {symlink_target}")
            os.remove(burr_examples_dir)
        else:
            shutil.rmtree(burr_examples_dir)

    # Copy the 4 required examples
    print("  Copying examples to burr/examples/...")
    os.makedirs(burr_examples_dir, exist_ok=True)

    # Copy __init__.py
    init_src = os.path.join(source_examples_dir, "__init__.py")
    if os.path.exists(init_src):
        shutil.copy2(init_src, os.path.join(burr_examples_dir, "__init__.py"))

    # Copy example directories
    for example_dir in REQUIRED_EXAMPLES[1:]:  # Skip __init__.py
        src_path = os.path.join(source_examples_dir, example_dir)
        dest_path = os.path.join(burr_examples_dir, example_dir)

        if os.path.exists(src_path) and os.path.isdir(src_path):
            shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
            print(f"    ✓ Copied {example_dir}")

    return (True, was_symlink, symlink_target)


def _cleanup_wheel_contents(was_symlink: bool, symlink_target: Optional[str]) -> None:
    """Restore burr/examples symlink after wheel build."""
    burr_examples_dir = "burr/examples"

    if os.path.exists(burr_examples_dir):
        shutil.rmtree(burr_examples_dir)

        if was_symlink and symlink_target:
            print(f"  Restoring symlink: burr/examples -> {symlink_target}")
            os.symlink(symlink_target, burr_examples_dir)
            print("    ✓ Symlink restored")


def _build_wheel_from_current_dir(version: str, output_dir: str = "dist") -> str:
    """Build wheel from current directory (matches what voters do).

    This is MUCH simpler than the old approach:
    - No temp directory extraction
    - No copying UI between directories
    - Just build in place and clean up
    """
    _print_step(1, 3, "Building UI artifacts")
    _build_ui_artifacts()

    _print_step(2, 3, "Preparing wheel contents")
    copied, was_symlink, symlink_target = _prepare_wheel_contents()

    _print_step(3, 3, "Building wheel with flit")

    try:
        env = os.environ.copy()
        env["FLIT_USE_VCS"] = "0"

        _run_command(
            ["flit", "build", "--format", "wheel"],
            description="",
            error_message="Wheel build failed",
            success_message="Wheel built successfully",
            env=env,
        )

        # Find the wheel
        wheel_pattern = f"dist/apache_burr-{version}*.whl"
        wheel_files = glob.glob(wheel_pattern)

        if not wheel_files:
            _fail(f"No wheel found matching: {wheel_pattern}")

        wheel_path = wheel_files[0]
        print(f"    ✓ Wheel created: {os.path.basename(wheel_path)}")

        return wheel_path

    finally:
        # Always restore symlinks
        if copied:
            _cleanup_wheel_contents(was_symlink, symlink_target)


def _verify_wheel(wheel_path: str) -> bool:
    """Verify wheel contents are correct."""
    import zipfile

    print(f"  Verifying wheel contents: {os.path.basename(wheel_path)}")

    try:
        with zipfile.ZipFile(wheel_path, "r") as whl:
            file_list = whl.namelist()

            # Check for UI build artifacts
            ui_files = [f for f in file_list if "burr/tracking/server/build/" in f]
            if not ui_files:
                print("    ✗ No UI build artifacts found")
                return False
            print(f"    ✓ Found {len(ui_files)} UI build files")

            # Check for required examples
            for example in REQUIRED_EXAMPLES:
                prefix = f"burr/examples/{example}"
                example_files = [f for f in file_list if f.startswith(prefix)]
                if not example_files:
                    print(f"    ✗ Required example not found: {example}")
                    return False

            print("    ✓ All 4 required examples found")
            print(f"    ✓ Wheel contains {len(file_list)} total files")
            return True

    except Exception as e:
        print(f"    ✗ Error verifying wheel: {e}")
        return False


def _verify_wheel_with_twine(wheel_path: str) -> bool:
    """Verify wheel metadata and package validity using twine."""
    print(f"  Verifying wheel with twine: {os.path.basename(wheel_path)}")

    try:
        subprocess.run(
            ["twine", "check", wheel_path],
            check=True,
            capture_output=True,
            text=True,
        )
        print("    ✓ Twine check passed")
        print("    ✓ Wheel metadata is valid")
        return True
    except subprocess.CalledProcessError as e:
        print("\n❌ Twine metadata validation failed\n")
        print("Twine output:")
        if e.stdout:
            print(e.stdout)
        if e.stderr:
            print(e.stderr)
        _fail("Wheel failed twine validation - see output above for details")


# ============================================================================
# Upload to Apache SVN
# ============================================================================


def _collect_all_artifacts(version: str, output_dir: str = "dist") -> list[str]:
    """Collect all artifacts for upload."""
    if not os.path.exists(output_dir):
        return []

    artifacts = []
    for filename in os.listdir(output_dir):
        # Match both incubating artifacts and wheel (which doesn't have -incubating suffix)
        version_match = f"{version}-incubating" in filename or f"{version}" in filename
        if version_match:
            if any(filename.endswith(ext) for ext in [".tar.gz", ".whl", ".asc", ".sha512"]):
                artifacts.append(os.path.join(output_dir, filename))

    return sorted(artifacts)


def _upload_to_svn(
    version: str,
    rc_num: str,
    apache_id: str,
    artifacts: list[str],
    dry_run: bool = False,
) -> Optional[str]:
    """Upload artifacts to Apache SVN distribution repository."""
    svn_url = f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/{version}-incubating-RC{rc_num}"

    if dry_run:
        print(f"\n[DRY RUN] Would upload to: {svn_url}")
        return svn_url

    print(f"Uploading to: {svn_url}")

    try:
        # Create directory
        subprocess.run(
            [
                "svn",
                "mkdir",
                "--parents",
                "-m",
                f"Creating directory for {version}-incubating-RC{rc_num}",
                svn_url,
            ],
            check=True,
        )

        # Upload each file
        for file_path in artifacts:
            filename = os.path.basename(file_path)
            print(f"  Uploading {filename}...")
            subprocess.run(
                [
                    "svn",
                    "import",
                    file_path,
                    f"{svn_url}/{filename}",
                    "-m",
                    f"Adding {filename}",
                    "--username",
                    apache_id,
                ],
                check=True,
            )

        print(f"\n✅ Artifacts uploaded to: {svn_url}")
        return svn_url

    except subprocess.CalledProcessError as e:
        print(f"Error during SVN upload: {e}")
        return None


def _generate_vote_email(version: str, rc_num: str, svn_url: str) -> str:
    """Generate [VOTE] email template."""
    version_with_incubating = f"{version}-incubating"
    tag = f"v{version}-incubating-RC{rc_num}"

    return f"""[VOTE] Release Apache {PROJECT_SHORT_NAME} {version_with_incubating} (RC{rc_num})

Hi all,

This is a call for a vote on releasing Apache {PROJECT_SHORT_NAME} {version_with_incubating},
release candidate {rc_num}.

The artifacts for this release candidate can be found at:
{svn_url}

The Git tag to be voted upon is:
{tag}

Release artifacts are signed with the release manager's GPG key. The KEYS file is available at:
https://downloads.apache.org/incubator/{PROJECT_SHORT_NAME}/KEYS

Please download, verify, and test the release candidate.

For detailed step-by-step instructions on how to verify this release, please see the
"For Voters: Verifying a Release" section in the scripts/README.md file within the
source archive.

The vote will run for a minimum of 72 hours.
Please vote:

[ ] +1 Release this package as Apache {PROJECT_SHORT_NAME} {version_with_incubating}
[ ] +0 No opinion
[ ] -1 Do not release this package because... (reason required)

Checklist for reference:
[ ] Download links are valid
[ ] Checksums and signatures are valid
[ ] LICENSE/NOTICE files exist
[ ] No unexpected binary files in source
[ ] All source files have ASF headers
[ ] Can compile from source

On behalf of the Apache {PROJECT_SHORT_NAME} PPMC,
[Your Name]
"""


# ============================================================================
# Command Handlers
# ============================================================================


def cmd_archive(args) -> bool:
    """Handle 'archive' subcommand."""
    _print_section(f"Creating Git Archive - v{args.version}-RC{args.rc_num}")
    skip_signing = getattr(args, "skip_signing", False)

    _verify_project_root()
    _validate_version(args.version)
    _check_git_working_tree()

    archive_path = _create_git_archive(
        args.version, args.rc_num, args.output_dir, skip_signing=skip_signing
    )
    print(f"\n✅ Archive created: {archive_path}")
    return True


def cmd_sdist(args) -> bool:
    """Handle 'sdist' subcommand."""
    _print_section(f"Building Source Distribution - v{args.version}-RC{args.rc_num}")
    skip_signing = getattr(args, "skip_signing", False)

    _verify_project_root()
    _validate_version(args.version)

    sdist_path = _build_sdist_from_git(args.version, args.output_dir)

    _print_step(2, 2, "Signing sdist" if not skip_signing else "Checksumming sdist")
    _sign_artifact(sdist_path, skip_signing=skip_signing)

    if not _verify_artifact_complete(sdist_path, skip_signing=skip_signing):
        _fail("sdist verification failed!")

    print(f"\n✅ Source distribution created: {sdist_path}")
    return True


def cmd_wheel(args) -> bool:
    """Handle 'wheel' subcommand."""
    _print_section(f"Building Wheel - v{args.version}-RC{args.rc_num}")
    skip_signing = getattr(args, "skip_signing", False)

    _verify_project_root()
    _validate_version(args.version)

    wheel_path = _build_wheel_from_current_dir(args.version, args.output_dir)

    print("\nVerifying wheel with twine...")
    if not _verify_wheel_with_twine(wheel_path):
        _fail("Twine verification failed!")

    print("\nVerifying wheel contents...")
    if not _verify_wheel(wheel_path):
        _fail("Wheel verification failed!")

    print("\nSigning wheel..." if not skip_signing else "\nChecksumming wheel...")
    _sign_artifact(wheel_path, skip_signing=skip_signing)

    if not _verify_artifact_complete(wheel_path, skip_signing=skip_signing):
        _fail("Wheel signature/checksum verification failed!")

    print(f"\n✅ Wheel created and verified: {os.path.basename(wheel_path)}")
    return True


def cmd_upload(args) -> bool:
    """Handle 'upload' subcommand."""
    _print_section(f"Uploading Artifacts - v{args.version}-RC{args.rc_num}")

    artifacts = _collect_all_artifacts(args.version, args.artifacts_dir)
    if not artifacts:
        _fail(f"No artifacts found in {args.artifacts_dir}")

    print(f"Found {len(artifacts)} artifact(s):")
    for artifact in artifacts:
        print(f"  - {os.path.basename(artifact)}")

    svn_url = _upload_to_svn(
        args.version, args.rc_num, args.apache_id, artifacts, dry_run=args.dry_run
    )

    if not svn_url:
        return False

    return True


def cmd_verify(args) -> bool:
    """Handle 'verify' subcommand."""
    _print_section(f"Verifying Artifacts - v{args.version}-RC{args.rc_num}")

    artifacts = _collect_all_artifacts(args.version, args.artifacts_dir)

    if not artifacts:
        print(f"⚠️  No artifacts found in {args.artifacts_dir}")
        return False

    all_valid = True
    for artifact in artifacts:
        if artifact.endswith((".asc", ".sha512")):
            continue  # Skip signature/checksum files
        if not _verify_artifact_complete(artifact):
            all_valid = False

    if all_valid:
        print("✅ All artifacts verified successfully!")
    else:
        print("❌ Some artifacts failed verification")

    return all_valid


def cmd_all(args) -> bool:
    """Handle 'all' subcommand - run complete workflow."""
    _print_section(f"Apache Burr Release Process - v{args.version}-RC{args.rc_num}")

    if args.dry_run:
        print("*** DRY RUN MODE ***\n")
    skip_signing = getattr(args, "skip_signing", False)
    if skip_signing:
        print("*** SKIP SIGNING MODE ***\n")

    _verify_project_root()
    _validate_version(args.version)
    _check_git_working_tree()

    # Step 1: Git Archive
    _print_step(1, 4, "Creating git archive")
    _create_git_archive(args.version, args.rc_num, args.output_dir, skip_signing=skip_signing)

    # Step 2: Build sdist
    _print_step(2, 4, "Building sdist")
    sdist_path = _build_sdist_from_git(args.version, args.output_dir)
    _sign_artifact(sdist_path, skip_signing=skip_signing)
    if not _verify_artifact_complete(sdist_path, skip_signing=skip_signing):
        _fail("sdist verification failed!")

    # Step 3: Build wheel
    _print_step(3, 4, "Building wheel")
    wheel_path = _build_wheel_from_current_dir(args.version, args.output_dir)
    if not _verify_wheel_with_twine(wheel_path):
        _fail("Twine verification failed!")
    _sign_artifact(wheel_path, skip_signing=skip_signing)
    if not _verify_wheel(wheel_path) or not _verify_artifact_complete(
        wheel_path, skip_signing=skip_signing
    ):
        _fail("Wheel verification failed!")

    # Step 4: Upload (if not disabled)
    if not args.no_upload:
        _print_step(4, 4, "Uploading to Apache SVN")
        all_artifacts = _collect_all_artifacts(args.version, args.output_dir)
        svn_url = _upload_to_svn(
            args.version, args.rc_num, args.apache_id, all_artifacts, dry_run=args.dry_run
        )
        if not svn_url:
            _fail("SVN upload failed!")
    else:
        svn_url = f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/{args.version}-incubating-RC{args.rc_num}"
        if args.dry_run:
            print(f"\n[DRY RUN] Would upload to: {svn_url}")

    # Generate email template
    _print_section("Release Complete!")
    email_content = _generate_vote_email(args.version, args.rc_num, svn_url)
    print(email_content)

    return True


# ============================================================================
# CLI Entry Point
# ============================================================================


def main():
    """Main entry point."""
    parser = argparse.ArgumentParser(description="Apache Burr Release Script (Simplified)")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # archive subcommand
    archive_parser = subparsers.add_parser("archive", help="Create git archive")
    archive_parser.add_argument("version", help="Version (e.g., '0.41.0')")
    archive_parser.add_argument("rc_num", help="RC number (e.g., '0')")
    archive_parser.add_argument("--output-dir", default="dist", help="Output directory")
    archive_parser.add_argument(
        "--skip-signing",
        action="store_true",
        help="Skip GPG signing (for CI). SHA512 checksum is still generated.",
    )

    # sdist subcommand
    sdist_parser = subparsers.add_parser("sdist", help="Build source distribution")
    sdist_parser.add_argument("version", help="Version")
    sdist_parser.add_argument("rc_num", help="RC number")
    sdist_parser.add_argument("--output-dir", default="dist")
    sdist_parser.add_argument("--skip-signing", action="store_true")

    # wheel subcommand
    wheel_parser = subparsers.add_parser("wheel", help="Build wheel")
    wheel_parser.add_argument("version", help="Version")
    wheel_parser.add_argument("rc_num", help="RC number")
    wheel_parser.add_argument("--output-dir", default="dist")
    wheel_parser.add_argument("--skip-signing", action="store_true")

    # upload subcommand
    upload_parser = subparsers.add_parser("upload", help="Upload to SVN")
    upload_parser.add_argument("version", help="Version")
    upload_parser.add_argument("rc_num", help="RC number")
    upload_parser.add_argument("apache_id", help="Apache ID")
    upload_parser.add_argument("--artifacts-dir", default="dist")
    upload_parser.add_argument("--dry-run", action="store_true")

    # verify subcommand
    verify_parser = subparsers.add_parser("verify", help="Verify artifacts")
    verify_parser.add_argument("version", help="Version")
    verify_parser.add_argument("rc_num", help="RC number")
    verify_parser.add_argument("--artifacts-dir", default="dist")

    # all subcommand
    all_parser = subparsers.add_parser("all", help="Run complete workflow")
    all_parser.add_argument("version", help="Version")
    all_parser.add_argument("rc_num", help="RC number")
    all_parser.add_argument("apache_id", help="Apache ID")
    all_parser.add_argument("--output-dir", default="dist")
    all_parser.add_argument("--dry-run", action="store_true")
    all_parser.add_argument("--no-upload", action="store_true")
    all_parser.add_argument(
        "--skip-signing",
        action="store_true",
        help="Skip GPG signing (for CI). SHA512 checksum is still generated.",
    )

    args = parser.parse_args()

    # Validate environment
    _validate_environment_for_command(args)

    # Dispatch to command handler
    try:
        if args.command == "archive":
            success = cmd_archive(args)
        elif args.command == "sdist":
            success = cmd_sdist(args)
        elif args.command == "wheel":
            success = cmd_wheel(args)
        elif args.command == "upload":
            success = cmd_upload(args)
        elif args.command == "verify":
            success = cmd_verify(args)
        elif args.command == "all":
            success = cmd_all(args)
        else:
            _fail(f"Unknown command: {args.command}")
    except KeyboardInterrupt:
        print("\n\n⚠️  Interrupted by user")
        sys.exit(130)
    except Exception as e:
        print(f"\n❌ Unexpected error: {e}")
        import traceback

        traceback.print_exc()
        sys.exit(1)

    if success:
        print("\n✅ Command completed successfully!")
        sys.exit(0)
    else:
        print("\n❌ Command failed.")
        sys.exit(1)


if __name__ == "__main__":
    main()
