"""Implementation of the 'do-release' command.

The do-release command combines update + commit + release into one atomic workflow.
It updates version files, commits the changes, creates a tag, publishes to PyPI,
and creates a GitHub release.

Hooks:
    The release workflow supports pre_release and post_release hooks that can
    execute arbitrary shell commands at specific points in the release process.

    - pre_release: Runs before Phase 1 (before updating version files)
    - post_release: Runs after Phase 5 (after GitHub release creation)

    Hook commands can use template variables:
    - {version}: The version being released (e.g., "1.2.3")
    - {tag}: The tag name (e.g., "v1.2.3")
    - {project_path}: Path to the project directory
"""

from __future__ import annotations

import asyncio
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING

from rich.panel import Panel

from releasio.config import load_config
from releasio.core.commits import calculate_bump, filter_skip_release_commits, parse_commits
from releasio.core.version import BumpType, Version
from releasio.vcs import GitRepository

if TYPE_CHECKING:
    from rich.console import Console

    from releasio.config.models import ReleasePyConfig


def _run_hook(
    hook_commands: list[str],
    hook_name: str,
    *,
    version: str,
    tag: str,
    project_path: Path,
    console: Console,
    err_console: Console,
) -> bool:
    """Execute release hook commands.

    Hook commands can use template variables: {version}, {tag}, {project_path}

    Args:
        hook_commands: List of shell commands to execute
        hook_name: Name of the hook for logging (e.g., "pre_release")
        version: Version being released
        tag: Tag name
        project_path: Path to the project directory
        console: Console for standard output
        err_console: Console for error output

    Returns:
        True if all hooks succeeded or list was empty, False if any failed
    """
    if not hook_commands:
        return True

    for hook_command in hook_commands:
        # Substitute template variables
        expanded_cmd = (
            hook_command.replace("{version}", version)
            .replace("{tag}", tag)
            .replace("{project_path}", str(project_path))
        )

        console.print(f"  Running {hook_name} hook: [cyan]{expanded_cmd}[/]")

        try:
            result = subprocess.run(
                expanded_cmd,
                shell=True,
                cwd=project_path,
                capture_output=True,
                text=True,
                check=True,
            )
            if result.stdout.strip():
                console.print(f"  [dim]{result.stdout.strip()}[/]")
            console.print(f"  [green]✓[/] {hook_name} hook completed")
        except subprocess.CalledProcessError as e:
            err_console.print(f"  [red]✗[/] {hook_name} hook failed")
            if e.stderr:
                err_console.print(f"  [dim]{e.stderr.strip()}[/]")
            return False

    return True


def run_do_release(
    path: str | None,
    execute: bool,
    skip_publish: bool,
    version_override: str | None,
    prerelease: str | None,
    console: Console,
    err_console: Console,
) -> None:
    """Run the complete release workflow: update + commit + release.

    Args:
        path: Optional path to project directory
        execute: Whether to actually apply changes (default is dry-run)
        skip_publish: Whether to skip PyPI publishing
        version_override: Manual version override (e.g., "2.0.0")
        prerelease: Pre-release identifier (e.g., "alpha", "beta", "rc")
        console: Console for standard output
        err_console: Console for error output
    """
    project_path = Path(path) if path else Path.cwd()

    # Load configuration
    try:
        config = load_config(project_path)
    except Exception as e:
        err_console.print(f"[red]Error loading config:[/] {e}")
        raise SystemExit(1) from e

    # Initialize git repository
    try:
        repo = GitRepository(project_path)
    except Exception as e:
        err_console.print(f"[red]Error:[/] {e}")
        raise SystemExit(1) from e

    # Must be clean (no uncommitted changes) unless allow_dirty is set
    if not config.allow_dirty and repo.is_dirty():
        err_console.print(
            "[red]Error:[/] Repository has uncommitted changes.\n"
            "Commit or stash them, or use [cyan]allow_dirty = true[/] in config."
        )
        raise SystemExit(1)

    # Must be on default branch
    current_branch = repo.get_current_branch()
    if current_branch != config.default_branch:
        err_console.print(
            f"[red]Error:[/] Must be on [cyan]{config.default_branch}[/] branch "
            f"(currently on [yellow]{current_branch}[/])."
        )
        raise SystemExit(1)

    # Get current version
    from releasio.config.loader import get_project_name, get_project_version

    try:
        project_name = get_project_name(project_path)
        current_version_str = get_project_version(project_path)
        current_version = Version.parse(current_version_str)
    except Exception as e:
        err_console.print(f"[red]Error getting project info:[/] {e}")
        raise SystemExit(1) from e

    # Get latest tag
    tag_pattern = f"{config.version.tag_prefix}*"
    latest_tag = repo.get_latest_tag(tag_pattern)

    # Detect first release (no existing tags)
    is_first_release = latest_tag is None

    # Get commits since last tag
    commits = repo.get_commits_since_tag(latest_tag)

    if not commits:
        console.print("[yellow]No commits found since last release. Nothing to do.[/]")
        return

    # Filter out commits with skip release markers
    commits = filter_skip_release_commits(commits, config.commits.skip_release_patterns)

    if not commits:
        console.print("[yellow]All commits have skip release markers. Nothing to do.[/]")
        return

    # Parse commits and calculate bump
    parsed = parse_commits(commits, config.commits)
    bump_type = calculate_bump(parsed, config.commits)

    # Handle manual version override
    if version_override:
        try:
            next_version = Version.parse(version_override)
        except Exception as e:
            err_console.print(f"[red]Invalid version format:[/] {e}")
            raise SystemExit(1) from e
    elif is_first_release:
        next_version = Version.parse(config.version.initial_version)
    else:
        if bump_type == BumpType.NONE:
            console.print(
                "[yellow]No releasable changes found (only non-release commit types).[/]\n"
                "[dim]Use [cyan]--version[/] to force a specific version.[/]"
            )
            return
        next_version = current_version.bump(bump_type)

    # Apply pre-release identifier if specified
    effective_prerelease = prerelease or config.get_effective_prerelease(current_branch)
    if effective_prerelease:
        next_version = next_version.with_prerelease(effective_prerelease)
        branch_config = config.get_branch_config(current_branch)
        if branch_config and branch_config.prerelease:
            console.print(
                f"[dim]Auto-detected pre-release channel [cyan]{effective_prerelease}[/] "
                f"from branch [cyan]{current_branch}[/][/]"
            )

    # Create tag name
    tag_name = next_version.with_tag_prefix(config.version.tag_prefix)

    # Check if tag already exists
    if repo.tag_exists(tag_name):
        err_console.print(
            f"[red]Error:[/] Tag [cyan]{tag_name}[/] already exists.\n"
            "This version may have already been released."
        )
        raise SystemExit(1)

    # Dry-run mode: show preview
    if not execute:
        _show_preview(
            console=console,
            project_name=project_name,
            current_version=current_version,
            next_version=next_version,
            tag_name=tag_name,
            is_first_release=is_first_release,
            skip_publish=skip_publish,
            config=config,
            commits_count=len(commits),
        )
        return

    # Execute the full release workflow
    console.print()
    if is_first_release:
        console.print(f"[bold]🎉 First release of {project_name} v{next_version}![/]\n")
    else:
        console.print(
            f"[bold]Releasing {project_name}: "
            f"[cyan]{current_version}[/] → [green]{next_version}[/][/]\n"
        )

    # Run pre_release hook if configured
    if config.hooks.pre_release:
        console.print("[bold]Pre-release hook[/]")
        if not _run_hook(
            config.hooks.pre_release,
            "pre_release",
            version=str(next_version),
            tag=tag_name,
            project_path=project_path,
            console=console,
            err_console=err_console,
        ):
            err_console.print(
                "[red]Error:[/] Pre-release hook failed. Release aborted.\n"
                "Fix the issue and try again."
            )
            raise SystemExit(1)
        console.print()

    # Track modified files for commit
    files_to_commit: list[Path] = []

    # Phase 1: Update version files
    console.print("[bold]Phase 1: Updating version files[/]")
    files_to_commit = _perform_update(
        project_path=project_path,
        next_version=next_version,
        config=config,
        repo=repo,
        console=console,
        err_console=err_console,
    )

    # Phase 2: Commit changes
    console.print("\n[bold]Phase 2: Committing changes[/]")
    commit_message = f"chore(release): prepare v{next_version}"
    repo.commit(commit_message, files_to_commit)
    console.print(f"  [green]✓[/] Committed: {commit_message}")

    # Phase 3: Create and push tag
    console.print("\n[bold]Phase 3: Creating tag[/]")
    repo.create_tag(tag_name, message=f"Release {tag_name}")
    repo.push_tag(tag_name)
    console.print(f"  [green]✓[/] Created and pushed tag {tag_name}")

    # Track success/failure of each phase
    pypi_success = True
    github_success = True
    github_result: str = ""

    # Phase 4: Build and publish
    if not skip_publish and config.publish.enabled:
        console.print("\n[bold]Phase 4: Publishing to PyPI[/]")
        pypi_success, _pypi_error = _perform_publish(
            project_path=project_path,
            project_name=project_name,
            next_version=next_version,
            config=config,
            console=console,
            err_console=err_console,
        )
    elif skip_publish:
        console.print("\n[bold]Phase 4: Skipping publish (--skip-publish)[/]")

    # Phase 5: Create GitHub release (always attempt, even if PyPI failed)
    console.print("\n[bold]Phase 5: Creating GitHub release[/]")
    github_success, github_result = _create_github_release(
        project_name=project_name,
        next_version=next_version,
        tag_name=tag_name,
        config=config,
        repo=repo,
        console=console,
        err_console=err_console,
    )

    # Run post_release hook if configured (runs regardless of success/failure)
    post_release_success = True
    if config.hooks.post_release:
        console.print("\n[bold]Post-release hook[/]")
        post_release_success = _run_hook(
            config.hooks.post_release,
            "post_release",
            version=str(next_version),
            tag=tag_name,
            project_path=project_path,
            console=console,
            err_console=err_console,
        )
        if not post_release_success:
            err_console.print(
                "[yellow]Warning:[/] Post-release hook failed. "
                "Release completed but hook did not run successfully."
            )

    # Show summary
    console.print()

    # Determine overall status
    all_success = pypi_success and github_success
    pypi_skipped = skip_publish or not config.publish.enabled

    if all_success:
        # Full success
        console.print(
            Panel(
                f"[bold green]🎉 Successfully released {project_name} v{next_version}![/]\n\n"
                f"[bold]GitHub Release:[/] {github_result}\n"
                + (
                    f"[bold]PyPI:[/] https://pypi.org/project/{project_name}/{next_version}/\n"
                    if not pypi_skipped
                    else ""
                )
                + "\nThank you for using releasio! ⭐",
                title="[bold green]Release Complete[/]",
                border_style="green",
            )
        )
    else:
        # Partial success - show what worked and what didn't
        summary_lines = [
            f"[bold yellow]⚠ Release {project_name} v{next_version} partially completed[/]\n"
        ]

        # Always show these as they always succeed if we got here
        summary_lines.append("[green]✓[/] Version updated")
        summary_lines.append("[green]✓[/] Changes committed")
        summary_lines.append(f"[green]✓[/] Tag {tag_name} created and pushed")

        if pypi_skipped:
            summary_lines.append("[dim]- PyPI publish skipped[/]")
        elif pypi_success:
            summary_lines.append("[green]✓[/] Published to PyPI")
        else:
            summary_lines.append("[red]✗[/] PyPI publish failed")

        if github_success:
            summary_lines.append(f"[green]✓[/] GitHub release created: {github_result}")
        else:
            summary_lines.append("[red]✗[/] GitHub release failed")

        summary_lines.append("\n[bold]To complete the release manually:[/]")

        if not pypi_success and not pypi_skipped:
            summary_lines.append("  [cyan]PyPI:[/] uv publish  [dim](set PYPI_TOKEN first)[/]")

        if not github_success:
            summary_lines.append(
                f"  [cyan]GitHub:[/] Create release at https://github.com/{{owner}}/{{repo}}/releases/new?tag={tag_name}"
            )

        console.print(
            Panel(
                "\n".join(summary_lines),
                title="[bold yellow]Release Partially Complete[/]",
                border_style="yellow",
            )
        )

        # Exit with error code if anything failed
        raise SystemExit(1)


def _show_preview(
    console: Console,
    project_name: str,
    current_version: Version,
    next_version: Version,
    tag_name: str,
    is_first_release: bool,
    skip_publish: bool,
    config: ReleasePyConfig,
    commits_count: int,
) -> None:
    """Show dry-run preview of what would happen."""
    version_info = (
        f"🎉 First release: [green]{next_version}[/]"
        if is_first_release
        else f"[cyan]{current_version}[/] → [green]{next_version}[/]"
    )

    console.print()
    console.print(
        Panel(
            f"[bold]Package:[/] {project_name}\n"
            f"[bold]Version:[/] {version_info}\n"
            f"[bold]Tag:[/] {tag_name}\n"
            f"[bold]Commits:[/] {commits_count}\n"
            f"[bold]Publish:[/] {'No (--skip-publish)' if skip_publish else 'Yes (PyPI)'}",
            title="[yellow]Release Preview (Dry Run)[/]",
            border_style="yellow",
        )
    )
    console.print()
    console.print("[bold]Actions that would be performed:[/]")
    console.print("  1. Update version in [cyan]pyproject.toml[/]")
    console.print(f"  2. Generate changelog in [cyan]{config.changelog.path}[/]")
    console.print(f"  3. Commit: [cyan]chore(release): prepare v{next_version}[/]")
    console.print(f"  4. Create and push tag [cyan]{tag_name}[/]")
    if not skip_publish:
        console.print("  5. Build package with uv")
        console.print("  6. Publish to PyPI")
    console.print(f"  7. Create GitHub release for [cyan]{tag_name}[/]")
    console.print("\n[dim]Run with [cyan]--execute[/] to perform the release.[/]")


def _perform_update(
    project_path: Path,
    next_version: Version,
    config: ReleasePyConfig,
    repo: GitRepository,
    console: Console,
    err_console: Console,
) -> list[Path]:
    """Perform version and changelog updates. Returns list of modified files."""
    from releasio.project.pyproject import (
        detect_version_files,
        update_pyproject_version,
        update_version_file,
        update_version_in_plain_file,
    )

    files_modified: list[Path] = []

    # Update pyproject.toml
    try:
        update_pyproject_version(project_path, str(next_version))
        console.print("  [green]✓[/] Updated version in pyproject.toml")
        files_modified.append(project_path / "pyproject.toml")
    except Exception as e:
        err_console.print(f"[red]Error updating pyproject.toml:[/] {e}")
        raise SystemExit(1) from e

    # Update additional version files (explicitly configured)
    for version_file in config.version.version_files:
        version_file_path = project_path / version_file
        try:
            if version_file_path.name == "VERSION":
                update_version_in_plain_file(version_file_path, str(next_version))
            else:
                update_version_file(version_file_path, str(next_version))
            console.print(f"  [green]✓[/] Updated version in {version_file}")
            files_modified.append(version_file_path)
        except Exception as e:
            err_console.print(f"[red]Error updating {version_file}:[/] {e}")
            raise SystemExit(1) from e

    # Auto-detect and update version files if enabled
    if config.version.auto_detect_version_files:
        detected_files = detect_version_files(project_path)
        for version_file_path in detected_files:
            relative_path = version_file_path.relative_to(project_path)
            if relative_path in config.version.version_files:
                continue
            try:
                if version_file_path.name == "VERSION":
                    update_version_in_plain_file(version_file_path, str(next_version))
                else:
                    update_version_file(version_file_path, str(next_version))
                console.print(f"  [green]✓[/] Updated version in {relative_path} (auto-detected)")
                files_modified.append(version_file_path)
            except Exception as e:
                console.print(f"  [yellow]⚠[/] Could not update {relative_path}: {e}")

    # Update lock file if enabled
    if config.version.update_lock_file:
        from releasio.project.lockfile import (
            detect_package_manager,
            get_lock_file_path,
            should_update_lock_file,
            update_lock_file,
        )

        if should_update_lock_file(project_path):
            pkg_manager = detect_package_manager(project_path)
            success, message = update_lock_file(project_path, pkg_manager)
            if success:
                console.print(f"  [green]✓[/] {message}")
                lock_file = get_lock_file_path(project_path, pkg_manager)
                if lock_file and lock_file.exists():
                    files_modified.append(lock_file)
            else:
                console.print(f"  [yellow]⚠[/] {message}")

    # Generate changelog
    try:
        from releasio.core.changelog import generate_changelog

        github_repo_str: str | None = None
        try:
            owner, repo_name = repo.parse_github_remote()
            github_owner = config.github.owner or owner
            github_repo = config.github.repo or repo_name
            github_repo_str = f"{github_owner}/{github_repo}"
        except Exception:
            console.print(
                "  [dim][yellow]Note:[/] Could not detect GitHub repository. "
                "Changelog will not include PR links.[/]"
            )

        changelog_content = generate_changelog(
            repo=repo,
            version=next_version,
            config=config,
            github_repo=github_repo_str,
            console=console,
        )

        changelog_path = project_path / config.changelog.path
        if changelog_path.exists():
            existing = changelog_path.read_text()
            new_content = changelog_content + "\n" + existing
        else:
            new_content = changelog_content

        changelog_path.write_text(new_content)
        console.print(f"  [green]✓[/] Updated {config.changelog.path}")
        files_modified.append(changelog_path)
    except Exception as e:
        err_console.print(f"[red]Error generating changelog:[/] {e}")
        raise SystemExit(1) from e

    return files_modified


def _perform_publish(  # noqa: PLR0911
    project_path: Path,
    project_name: str,
    next_version: Version,
    config: ReleasePyConfig,
    console: Console,
    err_console: Console,
) -> tuple[bool, str | None]:
    """Build and publish package to PyPI.

    Returns:
        Tuple of (success, error_message). If success is True, error_message is None.
    """
    from releasio.publish.pypi import build_package, publish_package

    # Build
    custom_build = config.hooks.build
    if custom_build:
        console.print(f"  • Building package (custom: [dim]{custom_build}[/])...")
    else:
        console.print(f"  • Building package with {config.publish.tool}...")

    try:
        dist_files = build_package(
            project_path,
            custom_command=custom_build,
            version=str(next_version),
            tool=config.publish.tool,
            console=console,
        )
        console.print("  [green]✓[/] Built package")
    except Exception as e:
        error_msg = f"Failed to build package: {e}"
        err_console.print(f"  [red]✗[/] {error_msg}")
        return False, error_msg

    # Validate distribution files
    if config.publish.validate_before_publish:
        console.print("  • Validating distribution files...")
        from releasio.publish.pypi import validate_dist_files

        try:
            is_valid, validation_message = validate_dist_files(dist_files)
            if not is_valid:
                error_msg = f"Package validation failed: {validation_message}"
                err_console.print(f"  [red]✗[/] {error_msg}")
                return False, error_msg
            console.print("  [green]✓[/] Distribution files validated")
        except Exception as e:
            error_msg = f"Failed to validate package: {e}"
            err_console.print(f"  [red]✗[/] {error_msg}")
            return False, error_msg

    # Check if version already exists on PyPI
    if config.publish.check_existing_version:
        console.print("  • Checking if version exists on PyPI...")
        from releasio.publish.pypi import check_pypi_version_exists

        try:
            if check_pypi_version_exists(project_name, str(next_version)):
                error_msg = (
                    f"Version {next_version} already exists on PyPI. "
                    "This version may have already been published."
                )
                err_console.print(f"  [red]✗[/] {error_msg}")
                return False, error_msg
            console.print("  [green]✓[/] Version not yet published")
        except Exception as e:
            error_msg = f"Failed to check PyPI: {e}"
            err_console.print(f"  [red]✗[/] {error_msg}")
            return False, error_msg

    # Publish
    console.print("  • Publishing to PyPI...")
    try:
        publish_package(project_path, config.publish, dist_files=dist_files, console=console)
        console.print("  [green]✓[/] Published to PyPI")
        return True, None
    except Exception as e:
        error_str = str(e)
        # Provide helpful hints for common credential errors
        if "credentials" in error_str.lower() or "token" in error_str.lower():
            error_msg = (
                f"PyPI publish failed: {e}\n\n"
                "[bold yellow]Missing PyPI credentials![/] Set one of:\n"
                "  • [cyan]PYPI_TOKEN[/] environment variable (recommended)\n"
                "  • [cyan]UV_PUBLISH_TOKEN[/] environment variable\n"
                "  • Configure [cyan]~/.pypirc[/] file\n"
                "  • Use trusted publishing (GitHub Actions OIDC)"
            )
        elif "403" in error_str or "forbidden" in error_str.lower():
            error_msg = (
                f"PyPI publish failed: {e}\n\n"
                "[bold yellow]Access denied![/] Check that:\n"
                "  • Your token has upload permissions\n"
                "  • The package name is not already taken by another user"
            )
        else:
            error_msg = f"PyPI publish failed: {e}"
        err_console.print(f"  [red]✗[/] {error_msg}")
        return False, error_msg


def _create_github_release(
    project_name: str,
    next_version: Version,
    tag_name: str,
    config: ReleasePyConfig,
    repo: GitRepository,
    console: Console,
    err_console: Console,
) -> tuple[bool, str]:
    """Create GitHub release.

    Returns:
        Tuple of (success, release_url_or_error_message).
    """
    from releasio.forge.github import GitHubClient

    console.print("  • Creating GitHub release...")

    try:
        owner, repo_name = repo.parse_github_remote()
        github_owner = config.github.owner or owner
        github_repo = config.github.repo or repo_name
    except Exception as e:
        error_msg = f"Failed to parse GitHub remote: {e}"
        err_console.print(f"  [red]✗[/] {error_msg}")
        return False, error_msg

    github = GitHubClient(owner=github_owner, repo=github_repo)

    # Get previous tag for changelog range
    tag_pattern = f"{config.version.tag_prefix}*"
    previous_tag = repo.get_latest_tag(tag_pattern)

    # Generate changelog content for release body
    changelog_content: str | None = None
    contributors: list[str] = []
    github_usernames: list[str] = []

    if config.changelog.use_github_prs:
        try:
            ignore_authors = config.changelog.ignore_authors

            async def fetch_pr_changelog() -> tuple[str, list[str]]:
                pr_changelog = await github.generate_pr_based_changelog(
                    previous_tag, ignore_authors=ignore_authors
                )
                prs = await github.get_merged_prs_between_tags(previous_tag)
                pr_contributors = await github.get_contributors_from_prs(
                    prs, ignore_authors=ignore_authors
                )
                return pr_changelog, pr_contributors

            changelog_content, github_usernames = asyncio.run(fetch_pr_changelog())
        except Exception as e:
            console.print(f"  [yellow]⚠[/] Could not fetch PRs: {e}")
    else:
        try:
            from releasio.core.changelog import generate_changelog

            github_repo_str = f"{github_owner}/{github_repo}"
            changelog_content = generate_changelog(
                repo,
                next_version,
                config,
                github_repo=github_repo_str,
                console=console,
            )
        except Exception as e:
            console.print(
                f"  [yellow]⚠[/] Could not generate changelog: {e}\n"
                "    GitHub release will be created without changelog content."
            )

        contributors = repo.get_contributors_since_tag(previous_tag)
        github_usernames = repo.get_contributor_github_usernames(previous_tag)

    # Generate release body
    release_body = _generate_release_body(
        project_name,
        next_version,
        changelog_content=changelog_content,
        contributors=contributors,
        github_usernames=github_usernames,
    )

    async def create_release_with_assets() -> tuple[str, list[str]]:
        from mimetypes import guess_type

        # Create release
        release = await github.create_release(
            tag=tag_name,
            name=f"{project_name} v{next_version}",
            body=release_body,
            draft=config.github.draft_releases,
            prerelease=next_version.is_prerelease,
        )

        asset_urls = []
        # Upload assets if configured
        if config.github.release_assets and release.id:
            for pattern in config.github.release_assets:
                # Use Path.glob() for better pathlib integration
                asset_paths = list(repo.path.glob(pattern))
                for asset_path in asset_paths:
                    if asset_path.exists():
                        content_type = guess_type(str(asset_path))[0] or "application/octet-stream"
                        try:
                            url = await github.upload_release_asset(
                                release.id, asset_path, content_type=content_type
                            )
                            asset_urls.append(url)
                        except Exception as e:
                            console.print(f"  [yellow]⚠[/] Failed to upload {asset_path.name}: {e}")

        return release.url, asset_urls

    try:
        release_url, uploaded_assets = asyncio.run(create_release_with_assets())
    except Exception as e:
        error_str = str(e)
        # Provide helpful hints for common errors
        if "401" in error_str or "unauthorized" in error_str.lower():
            error_msg = (
                f"GitHub release failed: {e}\n\n"
                "[bold yellow]Missing or invalid GitHub token![/] Set one of:\n"
                "  • [cyan]GITHUB_TOKEN[/] environment variable (recommended)\n"
                "  • [cyan]GH_TOKEN[/] environment variable\n"
                "  • In GitHub Actions, use [cyan]secrets.GITHUB_TOKEN[/]"
            )
        elif "403" in error_str or "forbidden" in error_str.lower():
            error_msg = (
                f"GitHub release failed: {e}\n\n"
                "[bold yellow]Insufficient permissions![/] Ensure your token has:\n"
                "  • [cyan]contents: write[/] permission\n"
                "  • Access to this repository"
            )
        elif "404" in error_str or "not found" in error_str.lower():
            error_msg = (
                f"GitHub release failed: {e}\n\n"
                "[bold yellow]Repository not found![/] Check that:\n"
                "  • The repository exists\n"
                "  • Your token has access to it\n"
                "  • The remote URL is correct"
            )
        else:
            error_msg = f"GitHub release failed: {e}"
        err_console.print(f"  [red]✗[/] {error_msg}")
        return False, error_msg

    console.print("  [green]✓[/] Created GitHub release")
    if uploaded_assets:
        console.print(f"  [green]✓[/] Uploaded {len(uploaded_assets)} asset(s)")

    # TODO: Future Feature - Security Advisory Integration
    # See detailed implementation notes in release.py:299-336
    # Same security advisory logic should be applied here

    return True, release_url


def _generate_release_body(
    project_name: str,
    version: Version,
    changelog_content: str | None = None,
    contributors: list[str] | None = None,
    github_usernames: list[str] | None = None,
) -> str:
    """Generate GitHub release body."""
    import re

    lines: list[str] = []

    if changelog_content:
        content = changelog_content.strip()
        content = re.sub(r"^#+\s*\[?v?\d+\.\d+\.\d+.*?\]?.*?\n+", "", content, flags=re.MULTILINE)
        if content:
            lines.append(content)
            lines.append("")

    if github_usernames:
        lines.append("## Contributors")
        lines.append("")
        username_links = [f"[@{u}](https://github.com/{u})" for u in github_usernames]
        lines.append(" • ".join(username_links))
        lines.append("")
    elif contributors:
        lines.append("## Contributors")
        lines.append("")
        lines.append(", ".join(contributors))
        lines.append("")

    lines.append("## Installation")
    lines.append("")
    lines.append("```bash")
    lines.append(f"pip install {project_name}=={version}")
    lines.append("```")
    lines.append("")
    lines.append("Or with uv:")
    lines.append("")
    lines.append("```bash")
    lines.append(f"uv add {project_name}=={version}")
    lines.append("```")
    lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("_Released with [releasio](https://github.com/mikeleppane/release-py)_")

    return "\n".join(lines)
