#!/usr/bin/env bash
# toolkit module: cli/08_scaffold.sh
# 项目脚手架：生成基于 radp-bash-framework 的 CLI 项目

#######################################
# 创建新的 CLI 项目
# Arguments:
#   1 - project_name: 项目名称
#   2 - target_dir: 目标目录（可选，默认为当前目录下的项目名）
# Returns:
#   0 - 成功
#   1 - 失败
#######################################
radp_cli_scaffold_new() {
  local project_name="$1"
  local target_dir="${2:-$project_name}"

  # 验证项目名称
  if [[ ! "$project_name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then
    radp_log_error "Invalid project name: $project_name"
    radp_log_error "Project name must start with a letter and contain only letters, numbers, underscores, and hyphens."
    return 1
  fi

  # 检查目标目录
  if [[ -d "$target_dir" ]]; then
    if [[ -n "$(ls -A "$target_dir" 2>/dev/null)" ]]; then
      radp_log_error "Directory already exists and is not empty: $target_dir"
      return 1
    fi
  fi

  radp_log_info "Creating new CLI project: $project_name"

  # 创建目录结构
  mkdir -p "$target_dir"/{bin,src/main/shell/{commands,config,libs}}
  mkdir -p "$target_dir"/packaging/{copr,homebrew,obs/debian/source}
  mkdir -p "$target_dir"/.github/workflows

  # 生成入口脚本
  __radp_cli_scaffold_bin "$project_name" "$target_dir"

  # 生成示例命令
  __radp_cli_scaffold_commands "$project_name" "$target_dir"

  # 生成配置文件（包含版本配置）
  __radp_cli_scaffold_config "$project_name" "$target_dir"

  # 生成 README
  __radp_cli_scaffold_readme "$project_name" "$target_dir"

  # 生成 CLAUDE.md
  __radp_cli_scaffold_claude "$project_name" "$target_dir"

  # 生成 CONTRIBUTING.md
  __radp_cli_scaffold_contributing "$project_name" "$target_dir"

  # 生成 .gitignore
  __radp_cli_scaffold_gitignore "$target_dir"

  # 生成 CHANGELOG
  __radp_cli_scaffold_changelog "$project_name" "$target_dir"

  # 生成安装脚本
  __radp_cli_scaffold_install "$project_name" "$target_dir"

  # 生成打包文件
  __radp_cli_scaffold_packaging "$project_name" "$target_dir"

  # 生成 GitHub workflows
  __radp_cli_scaffold_workflows "$project_name" "$target_dir"

  # 初始化项目元数据（用于 upgrade 命令）
  radp_cli_init_metadata "$target_dir" "$project_name" "$(radp_get_fw_install_version)"

  radp_log_info "Project created successfully: $target_dir"
  radp_log_info ""
  radp_log_info "Next steps:"
  radp_log_info "  cd $target_dir"
  radp_log_info "  ./bin/$project_name --help"
  radp_log_info ""
  radp_log_info "Add new commands by creating files in src/main/shell/commands/"
}

#######################################
# 生成入口脚本内容
# Arguments:
#   1 - project_name: 项目名称
# Outputs:
#   入口脚本内容（写入 stdout）
#######################################
radp_cli_entry_content() {
  local project_name="$1"
  cat <<ENTRY_SCRIPT
#!/usr/bin/env bash
set -euo pipefail

# 检查框架是否已安装
if ! command -v radp-bf &>/dev/null; then
  echo "Error: radp-bash-framework not found. Please install it first." >&2
  echo "  See: https://github.com/xooooooooox/radp-bash-framework" >&2
  exit 1
fi

# 设置应用名称和根目录
export RADP_APP_NAME="$project_name"
export RADP_APP_ROOT="\$(radp-bf path resolve "\${BASH_SOURCE[0]}")"
# shellcheck source=/dev/null
source "\$(radp-bf path launcher)" "\$@"
ENTRY_SCRIPT
}

#######################################
# 生成入口脚本（thin entry script）
# 所有 boilerplate 逻辑由 framework/launcher.sh 处理
#######################################
__radp_cli_scaffold_bin() {
  local project_name="$1"
  local target_dir="$2"

  radp_cli_entry_content "$project_name" >"$target_dir/bin/$project_name"
  chmod +x "$target_dir/bin/$project_name"
}

#######################################
# 生成 version.sh 内容
# Arguments:
#   1 - project_name: 项目名称
#   2 - version: 版本号（默认 v0.0.1）
# Outputs:
#   version.sh 内容（写入 stdout）
#######################################
radp_cli_version_cmd_content() {
  local project_name="$1"
  local version="${2:-v0.0.1}"
  cat <<VERSION_CMD
#!/usr/bin/env bash
# @cmd
# @desc Show version information

# Application version
# Update this value when releasing a new version
declare -gr gr_app_version="${version}"

cmd_version() {
    echo "${project_name} \$(radp_get_install_version "\${gr_app_version}")"
}
VERSION_CMD
}

#######################################
# 生成 completion.sh 内容
# Arguments:
#   1 - project_name: 项目名称
# Outputs:
#   completion.sh 内容（写入 stdout）
#######################################
radp_cli_completion_cmd_content() {
  local project_name="$1"
  cat <<COMPLETION_CMD
#!/usr/bin/env bash
# @cmd
# @desc Generate shell completion script
# @arg shell! Shell type (bash or zsh)
# @arg-values shell bash zsh
# @example completion bash > ~/.local/share/bash-completion/completions/${project_name}
# @example completion zsh > ~/.zfunc/_${project_name}

cmd_completion() {
    local shell="\${1:-}"

    if [[ -z "\$shell" ]]; then
        radp_log_error "Shell type required (bash or zsh)"
        return 1
    fi

    radp_cli_completion_generate "\$shell"
}
COMPLETION_CMD
}

#######################################
# 生成 upgrade.sh 内容
# Arguments:
#   1 - project_name: 项目名称
# Outputs:
#   upgrade.sh 内容（写入 stdout）
#######################################
radp_cli_upgrade_cmd_content() {
  local project_name="$1"
  cat <<UPGRADE_CMD
#!/usr/bin/env bash
# @cmd
# @desc Upgrade ${project_name} to the latest version
# @meta passthrough
# @flag --check Only check for updates
# @flag --force Force upgrade even if at latest
# @flag -y, --yes Skip confirmation prompt
# @option --version <version> Target specific version

cmd_upgrade() {
  radp_cli_upgrade_self "\$@"
}
UPGRADE_CMD
}

#######################################
# 生成 config.yaml 内容
# Arguments:
#   1 - project_name: 项目名称
#   2 - project_var: 项目变量名（连字符替换为下划线）
# Outputs:
#   config.yaml 内容（写入 stdout）
#######################################
radp_cli_config_yaml_content() {
  local project_name="$1"
  local project_var="$2"
  cat <<YAML_CONFIG
# $project_name configuration
# This file follows radp-bash-framework's configuration structure
# Priority: Environment variables (GX_*) > YAML values > defaults

radp:
  env: default

  # Framework settings override (optional)
  fw:
    banner-mode: off
    log:
      debug: false
      level: info
      console:
        enabled: false
      file:
        enabled: false

  # Application-specific extensions
  # Variables defined here will be available as gr_radp_extend_* in shell
  extend:
    ${project_var}:
      # Add your application-specific configuration here
      # Example: api_url: https://api.example.com
YAML_CONFIG
}

#######################################
# 生成 config-dev.yaml 内容
# Arguments:
#   1 - project_name: 项目名称
#   2 - project_var: 项目变量名（连字符替换为下划线）
# Outputs:
#   config-dev.yaml 内容（写入 stdout）
#######################################
radp_cli_config_dev_yaml_content() {
  local project_name="$1"
  local project_var="$2"
  cat <<YAML_DEV
# Development environment overrides for $project_name

radp:
  fw:
    log:
      debug: true
      level: debug

  extend:
    ${project_var}:
      # Development-specific overrides
YAML_DEV
}

#######################################
# 生成示例命令
#######################################
__radp_cli_scaffold_commands() {
  local project_name="$1"
  local target_dir="$2"

  # version 命令
  radp_cli_version_cmd_content "$project_name" >"$target_dir/src/main/shell/commands/version.sh"

  # completion 命令
  radp_cli_completion_cmd_content "$project_name" >"$target_dir/src/main/shell/commands/completion.sh"

  # hello 示例命令
  cat >"$target_dir/src/main/shell/commands/hello.sh" <<'HELLO_CMD'
#!/usr/bin/env bash
# @cmd
# @desc Say hello (example command)
# @arg name Name to greet
# @option -u, --uppercase Convert to uppercase
# @example hello
# @example hello World
# @example hello --uppercase World

cmd_hello() {
    local name="${1:-World}"
    local message="Hello, $name!"

    if [[ "${opt_uppercase:-}" == "true" ]]; then
        message="${message^^}"
    fi

    echo "$message"
}
HELLO_CMD

  # 应用级全局选项示例文件
  cat >"$target_dir/src/main/shell/commands/_globals.sh" <<'GLOBALS_CMD'
#!/usr/bin/env bash
# Application-level global options
# These options are available to all commands
#
# Usage: Define @global annotations here, similar to @option in command files
# Variables will be available as gopt_<name> (e.g., gopt_verbose)
#
# Syntax examples:
#   @global -v, --verbose Enable verbose output
#   @global -c, --config <dir> Configuration directory
#   @global -e, --env <name> Environment name [default: local]
#
# Note: These options can be placed before or after the command:
#   mycli -c /path list
#   mycli list -c /path
GLOBALS_CMD

  # upgrade 命令
  radp_cli_upgrade_cmd_content "$project_name" >"$target_dir/src/main/shell/commands/upgrade.sh"
}

#######################################
# 生成配置文件
# 遵循 radp-bash-framework 的 YAML 配置机制
#######################################
__radp_cli_scaffold_config() {
  local project_name="$1"
  local target_dir="$2"
  local project_var="${project_name//-/_}"

  # 生成 config.yaml（遵循 radp-bash-framework 的配置结构）
  radp_cli_config_yaml_content "$project_name" "$project_var" >"$target_dir/src/main/shell/config/config.yaml"

  # 生成环境特定配置示例
  radp_cli_config_dev_yaml_content "$project_name" "$project_var" >"$target_dir/src/main/shell/config/config-dev.yaml"

  # 生成 IDE code completion 支持文件
  cat >"$target_dir/src/main/shell/config/_ide.sh" <<'IDE_HINTS'
#!/usr/bin/env bash
# IDE code completion support for BashSupport Pro
# This file is not executed at runtime, only used for IDE navigation
#
# References the auto-generated _idecomp.sh which provides navigation to:
#   - Framework library functions (radp_*)
#   - Framework global variables (gr_fw_*, gr_radp_fw_*)
#   - User global variables (gr_radp_extend_*)
#   - User library functions
# Note: _idecomp.sh is auto-generated and should be in .gitignore
# shellcheck source=./_idecomp.sh
IDE_HINTS
}

#######################################
# 生成 README
#######################################
__radp_cli_scaffold_readme() {
  local project_name="$1"
  local target_dir="$2"

  cat >"$target_dir/README.md" <<README
# $project_name

A CLI tool built with [radp-bash-framework](https://github.com/xooooooooox/radp-bash-framework).

## Installation

### Homebrew (Recommended)

\`\`\`bash
brew tap xooooooooox/radp
brew install radp-bash-framework  # Prerequisite
brew install $project_name
\`\`\`

### Script

\`\`\`bash
curl -fsSL https://raw.githubusercontent.com/xooooooooox/$project_name/main/install.sh | bash
\`\`\`

### RPM (COPR)

\`\`\`bash
sudo dnf copr enable -y xooooooooox/radp
sudo dnf install -y $project_name
\`\`\`

## Quick Start

\`\`\`bash
# Show help
$project_name --help

# Show version
$project_name version

# Example command
$project_name hello World

# Verbose mode
$project_name -v hello World
\`\`\`

## Global Options

| Option | Description |
|--------|-------------|
| \`-v\`, \`--verbose\` | Enable verbose output |
| \`--debug\` | Enable debug output |
| \`-h\`, \`--help\` | Show help |

## Configuration

Configuration files are in \`src/main/shell/config/\`:

\`\`\`yaml
radp:
  env: default
  fw:
    log:
      level: info
  extend:
    ${project_name//-/_}:
      # Your custom config here
\`\`\`

Override with environment variables using \`GX_*\` prefix:

\`\`\`bash
GX_RADP_FW_LOG_DEBUG=true $project_name hello
\`\`\`

## Adding Commands

Create command files in \`src/main/shell/commands/\`:

\`\`\`bash
# src/main/shell/commands/mycommand.sh
# @cmd
# @desc My command description
# @arg name! Required argument

cmd_mycommand() {
    echo "Hello, \$1!"
}
\`\`\`

See [radp-bash-framework CLI Development](https://github.com/xooooooooox/radp-bash-framework/blob/main/docs/user-guide/cli-development.md) for details.

## Related Projects

- [radp-bash-framework](https://github.com/xooooooooox/radp-bash-framework) - Bash CLI framework
- [radp-vagrant-framework](https://github.com/xooooooooox/radp-vagrant-framework) - YAML-driven Vagrant framework
- [homelabctl](https://github.com/xooooooooox/homelabctl) - Homelab infrastructure CLI

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and release process.

## License

MIT
README
}

#######################################
# 生成 CLAUDE.md
#######################################
__radp_cli_scaffold_claude() {
  local project_name="$1"
  local target_dir="$2"
  local project_var="${project_name//-/_}"

  cat >"$target_dir/CLAUDE.md" <<CLAUDE
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

$project_name is a CLI tool built with [radp-bash-framework](https://github.com/xooooooooox/radp-bash-framework).

## Commands

\`\`\`bash
./bin/$project_name --help              # Show help
./bin/$project_name version             # Show version
./bin/$project_name hello <name>        # Example command
./bin/$project_name -v hello <name>     # Verbose mode
./bin/$project_name --debug hello       # Debug mode
\`\`\`

## Architecture

\`\`\`
bin/$project_name                       # Entry point (thin launcher)
    ↓
radp-bash-framework init.sh            # Framework initialization
    ↓
src/main/shell/commands/*.sh           # Command implementations
\`\`\`

### Key Directories

| Directory | Purpose |
|-----------|---------|
| \`bin/\` | Entry point script |
| \`src/main/shell/commands/\` | CLI commands |
| \`src/main/shell/config/\` | YAML configuration |
| \`src/main/shell/libs/\` | Custom libraries |

## Naming Conventions

| Pattern | Meaning | Example |
|---------|---------|---------|
| \`gr_*\` | Global readonly | \`gr_app_version\` |
| \`gw_*\` | Global writable | \`gw_some_state\` |
| \`radp_*\` | Framework API | \`radp_log_info\` |
| \`cmd_*\` | Command function | \`cmd_hello\` |

## Configuration

\`\`\`yaml
# src/main/shell/config/config.yaml
radp:
  env: default
  extend:
    $project_var:
      # Custom config
\`\`\`

Access config: \`\$gr_radp_extend_${project_var}_*\`

## Adding Commands

1. Create \`src/main/shell/commands/mycommand.sh\`
2. Add \`# @cmd\` annotation
3. Implement \`cmd_mycommand()\` function

See [radp-bash-framework docs](https://github.com/xooooooooox/radp-bash-framework/blob/main/docs/) for details.

## See Also

- [radp-bash-framework CLAUDE.md](https://github.com/xooooooooox/radp-bash-framework/blob/main/CLAUDE.md)
- [radp-bash-framework API Reference](https://github.com/xooooooooox/radp-bash-framework/blob/main/docs/reference/api.md)
CLAUDE
}

#######################################
# 生成 CONTRIBUTING.md
#######################################
__radp_cli_scaffold_contributing() {
  local project_name="$1"
  local target_dir="$2"

  cat >"$target_dir/CONTRIBUTING.md" <<'CONTRIBUTING'
# Contributing

## Development Setup

```bash
git clone https://github.com/xooooooooox/$project_name
cd $project_name
./bin/$project_name --help
```

## Project Structure

```
$project_name/
├── bin/                          # Entry point
├── src/main/shell/
│   ├── commands/                 # CLI commands
│   ├── config/                   # YAML configuration
│   └── libs/                     # Custom libraries
├── packaging/                    # Distribution packages
│   ├── copr/                     # Fedora COPR
│   ├── homebrew/                 # Homebrew formula
│   └── obs/                      # OpenSUSE OBS
└── .github/workflows/            # CI/CD workflows
```

## Adding Commands

1. Create `src/main/shell/commands/mycommand.sh`
2. Add annotations:
   ```bash
   # @cmd
   # @desc Command description
   # @arg name! Required argument
   # @option -f, --flag Option description
   ```
3. Implement `cmd_mycommand()` function

## Version Management

Version is defined in `src/main/shell/commands/version.sh`:

```bash
declare -gr gr_app_version="v0.0.1"
```

CI workflows automatically update this during releases.

## CI/CD Workflows

```
release-prep (manual trigger)
       │
       ▼
   PR merged
       │
       ▼
create-version-tag
       │
       ├──────────────────────┬──────────────────────┐
       ▼                      ▼                      ▼
update-spec-version    update-homebrew-tap    (GitHub Release)
       │
       ├──────────────┐
       ▼              ▼
build-copr-package  build-obs-package
```

### Release Process

1. Trigger `release-prep` workflow with bump_type (patch/minor/major)
2. Review and merge the generated PR
3. Subsequent workflows run automatically

### Required Secrets

#### Homebrew Tap

| Secret | Description |
|--------|-------------|
| `HOMEBREW_TAP_TOKEN` | GitHub PAT with `repo` scope |

#### COPR

| Secret | Description |
|--------|-------------|
| `COPR_LOGIN` | COPR API login |
| `COPR_TOKEN` | COPR API token |
| `COPR_USERNAME` | COPR username |
| `COPR_PROJECT` | COPR project name |

#### OBS

| Secret | Description |
|--------|-------------|
| `OBS_USERNAME` | OBS username |
| `OBS_PASSWORD` | OBS password/token |
| `OBS_PROJECT` | OBS project name |
| `OBS_PACKAGE` | OBS package name |

### Skipping Workflows

- Delete workflow file from `.github/workflows/`
- Or leave secrets unconfigured (workflow skips)

## Code Style

- Follow [radp-bash-framework code style](https://github.com/xooooooooox/radp-bash-framework/blob/main/docs/developer/code-style.md)
- Use `radp_log_*` functions for logging
- Quote variables: `"$var"`
CONTRIBUTING

  # Replace $project_name placeholder
  sed -i.bak "s/\$project_name/$project_name/g" "$target_dir/CONTRIBUTING.md"
  rm -f "$target_dir/CONTRIBUTING.md.bak"
}

#######################################
# 生成 .gitignore
#######################################
__radp_cli_scaffold_gitignore() {
  local target_dir="$1"

  cat >"$target_dir/.gitignore" <<'GITIGNORE'
# Auto-generated files
src/main/shell/config/config.sh
src/main/shell/config/_idecomp.sh
.radp-cli/checksums/

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Logs
*.log
logs/

# Temporary files
tmp/
temp/
*.tmp
*.bak
GITIGNORE
}

#######################################
# 生成 CHANGELOG
#######################################
__radp_cli_scaffold_changelog() {
  local project_name="$1"
  local target_dir="$2"

  cat >"$target_dir/CHANGELOG.md" <<'CHANGELOG'
# CHANGELOG

## v0.1.0 - Initial Release

- Initial release
CHANGELOG
}

#######################################
# 生成安装脚本
#######################################
__radp_cli_scaffold_install() {
  local project_name="$1"
  local target_dir="$2"
  local project_var="${project_name//-/_}"
  local project_upper="${project_var^^}"

  # Use content generator function
  radp_packaging_content_install_sh "$project_name" >"$target_dir/install.sh"

  # Replace placeholders
  sed -i.bak "s/__PROJECT_NAME__/${project_name}/g" "$target_dir/install.sh"
  sed -i.bak "s/__PROJECT_UPPER__/${project_upper}/g" "$target_dir/install.sh"
  sed -i.bak "s/__PROJECT_VAR__/${project_var}/g" "$target_dir/install.sh"
  rm -f "$target_dir/install.sh.bak"
  chmod +x "$target_dir/install.sh"
}

#######################################
# 生成打包文件
#######################################
__radp_cli_scaffold_packaging() {
  local project_name="$1"
  local target_dir="$2"
  local today
  today="$(date '+%a %b %d %Y')"

  # COPR and OBS spec files (using shared content generator)
  radp_packaging_content_spec "$project_name" "$today" >"$target_dir/packaging/copr/${project_name}.spec"
  radp_packaging_content_spec "$project_name" "$today" >"$target_dir/packaging/obs/${project_name}.spec"

  # Homebrew formula (using shared content generator)
  radp_packaging_content_homebrew "$project_name" >"$target_dir/packaging/homebrew/${project_name}.rb"

  # Debian packaging files
  radp_packaging_content_debian_control "$project_name" >"$target_dir/packaging/obs/debian/control"
  radp_packaging_content_debian_rules "$project_name" >"$target_dir/packaging/obs/debian/rules"
  radp_packaging_content_debian_install "$project_name" >"$target_dir/packaging/obs/debian/${project_name}.install"
  radp_packaging_content_debian_links "$project_name" >"$target_dir/packaging/obs/debian/${project_name}.links"
  radp_packaging_content_debian_changelog "$project_name" >"$target_dir/packaging/obs/debian/changelog"
  radp_packaging_content_debian_copyright "$project_name" >"$target_dir/packaging/obs/debian/copyright"
  echo "3.0 (quilt)" >"$target_dir/packaging/obs/debian/source/format"
}

#######################################
# 生成 GitHub workflows
#######################################
__radp_cli_scaffold_workflows() {
  local project_name="$1"
  local target_dir="$2"
  local project_var="${project_name//-/_}"

  mkdir -p "$target_dir/.github/workflows"

  radp_workflow_content_release_prep "$project_name" "$project_var" >"$target_dir/.github/workflows/release-prep.yml"
  radp_workflow_content_create_tag "$project_name" "$project_var" >"$target_dir/.github/workflows/create-version-tag.yml"
  radp_workflow_content_update_spec "$project_name" "$project_var" >"$target_dir/.github/workflows/update-spec-version.yml"
  radp_workflow_content_build_copr "$project_name" "$project_var" >"$target_dir/.github/workflows/build-copr-package.yml"
  radp_workflow_content_build_obs "$project_name" "$project_var" >"$target_dir/.github/workflows/build-obs-package.yml"
  radp_workflow_content_homebrew "$project_name" "$project_var" >"$target_dir/.github/workflows/update-homebrew-tap.yml"
  radp_workflow_content_attach_packages "$project_name" "$project_var" >"$target_dir/.github/workflows/attach-release-packages.yml"
  radp_workflow_content_cleanup_branches "$project_name" "$project_var" >"$target_dir/.github/workflows/cleanup-branches.yml"
}

#######################################
# Workflow content generators (reusable by scaffold and upgrade)
# 这些函数输出 workflow 内容到 stdout，可被 scaffold 和 upgrade 复用
#######################################

radp_workflow_content_release_prep() {
  local project_name="$1"
  local project_var="$2"

  cat <<'WORKFLOW'
name: Release prep

on:
  workflow_dispatch:
    inputs:
      bump_type:
        description: "Release version bump type"
        type: choice
        options:
          - patch
          - minor
          - major
          - manual
        default: patch
        required: true
      version:
        description: "Manual release version tag (vX.Y.Z) when bump_type=manual"
        required: false

permissions:
  contents: write
  pull-requests: write

jobs:
  release-prep:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Fetch tags
        run: git fetch --tags --force

      - name: Resolve version
        id: version
        run: |
          set -euo pipefail
          bump_type="${{ inputs.bump_type }}"
          manual_version="${{ inputs.version }}"

          # Get latest version tag from git
          latest_tag="$(git tag --list 'v*' --sort=-v:refname | head -n 1)"

          # Handle initial release (no tags)
          if [[ -z "${latest_tag}" ]]; then
            echo "No version tags found; using v0.0.0 as base for initial release."
            latest_tag="v0.0.0"
          fi

          if [[ ! "${latest_tag}" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
            echo "Latest tag '${latest_tag}' does not match vX.Y.Z" >&2
            exit 1
          fi
          major="${BASH_REMATCH[1]}"
          minor="${BASH_REMATCH[2]}"
          patch="${BASH_REMATCH[3]}"

          case "${bump_type}" in
            patch)
              version="v${major}.${minor}.$((patch + 1))"
              ;;
            minor)
              version="v${major}.$((minor + 1)).0"
              ;;
            major)
              version="v$((major + 1)).0.0"
              ;;
            manual)
              if [[ -z "${manual_version}" ]]; then
                echo "Manual bump_type requires inputs.version." >&2
                exit 1
              fi
              version="${manual_version}"
              ;;
            *)
              echo "Unsupported bump_type: ${bump_type}" >&2
              exit 1
              ;;
          esac

          if [[ ! "${version}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Version '${version}' does not match vX.Y.Z" >&2
            exit 1
          fi

          echo "version=${version}" >> "$GITHUB_OUTPUT"
          echo "latest=${latest_tag}" >> "$GITHUB_OUTPUT"

      - name: Create release branch
        id: branch
        run: |
          set -euo pipefail
          version="${{ steps.version.outputs.version }}"
          if git rev-parse "${version}" >/dev/null 2>&1; then
            echo "Tag ${version} already exists. Aborting release prep." >&2
            exit 1
          fi
          branch="workflow/${version}"
          reused=false
          if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then
            git checkout -b "${branch}" "origin/${branch}"
            echo "Reusing existing branch ${branch}"
            reused=true
          else
            git checkout -b "${branch}"
          fi
          echo "branch=${branch}" >> "$GITHUB_OUTPUT"
          echo "reused=${reused}" >> "$GITHUB_OUTPUT"

      - name: Update versions and changelog
        env:
          VERSION: ${{ steps.version.outputs.version }}
WORKFLOW

  # 这部分需要插入 project_name
  cat <<WORKFLOW
        run: |
          set -euo pipefail
          version="\${VERSION}"
          version_no_prefix="\${version#v}"

          # Update version.sh
          sed -i "s/^declare -gr gr_app_version=.*/declare -gr gr_app_version=\"\${version}\"/" src/main/shell/commands/version.sh

          # Update spec files
          sed -i "s/^Version:.*/Version:        \${version_no_prefix}/" packaging/copr/${project_name}.spec
          sed -i "s/^Version:.*/Version:        \${version_no_prefix}/" packaging/obs/${project_name}.spec

          # Update CHANGELOG.md
          python3 - <<'PY'
          import os
          import re
          import subprocess
          from pathlib import Path

          version = os.environ["VERSION"].strip()
          if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+\$", version):
            raise SystemExit(f"Invalid version: {version}")
          version_no_prefix = version[1:]

          def git(*args: str) -> str:
            return subprocess.check_output(["git", *args], text=True, errors="replace").strip()

          def try_git(*args: str) -> str:
            try:
              return git(*args)
            except subprocess.CalledProcessError:
              return ""

          changelog_path = Path("CHANGELOG.md")
          if not changelog_path.exists():
            raise SystemExit("CHANGELOG.md not found.")
          changelog_text = changelog_path.read_text()
          header_pattern = re.compile(rf"^##\\s+v?{re.escape(version_no_prefix)}(\\s|\$)")

          last_tag = try_git("describe", "--tags", "--abbrev=0", "--match", "v*")
          log_range = f"{last_tag}..HEAD" if last_tag else "HEAD"
          log_output = try_git("log", "--no-merges", "--pretty=%h %s", log_range)
          base_types = ["feat", "fix", "chore"]
          standard_types = base_types + ["docs", "refactor", "perf", "test", "build", "ci"]
          other_key = "other"
          entries_by_type = {t: [] for t in standard_types}
          entries_by_type[other_key] = []
          type_pattern = re.compile(r"^(?P<type>[A-Za-z][A-Za-z0-9_-]*)(\\([^\\)]*\\))?:\\s+(?P<desc>.+)\$")

          for line in log_output.splitlines():
            line = line.strip()
            if not line:
              continue
            parts = line.split(" ", 1)
            sha = parts[0]
            subject = parts[1] if len(parts) > 1 else ""
            subject_ascii = subject.encode("ascii", "ignore").decode("ascii").strip()
            if not subject_ascii:
              subject_ascii = "<non-ascii subject>"
            match = type_pattern.match(subject_ascii)
            if match:
              commit_type = match.group("type").lower()
              desc = match.group("desc").strip() or subject_ascii
            else:
              commit_type = other_key
              desc = subject_ascii
            if commit_type not in entries_by_type:
              commit_type = other_key
            entries_by_type[commit_type].append(desc)

          default_header = f"## {version}"
          include_types = [t for t in standard_types if entries_by_type[t]]
          if entries_by_type[other_key]:
            include_types.append(other_key)

          def build_section_lines(header_line: str, leading_blank: bool) -> list[str]:
            section = []
            if leading_blank:
              section.append("")
            section.append(header_line)
            section.append("")
            if not include_types:
              section.append("- TODO: no commits found; add summary manually.")
              return section
            for commit_type in include_types:
              section.append(f"### {commit_type}")
              section.append("")
              for entry in entries_by_type.get(commit_type, []):
                section.append(f"- {entry}")
              section.append("")
            while section and section[-1] == "":
              section.pop()
            return section

          lines = changelog_text.splitlines()
          start = None
          for idx, line in enumerate(lines):
            if header_pattern.match(line):
              start = idx
              break
          if start is not None:
            end = len(lines)
            for idx in range(start + 1, len(lines)):
              if lines[idx].startswith("## "):
                end = idx
                break
            existing_section = "\\n".join(lines[start:end])
            todo_markers = (
              "TODO: curate and rewrite this list before release.",
              "TODO: no commits found; add summary manually.",
            )
            if any(marker in existing_section for marker in todo_markers):
              header_line = lines[start]
              new_lines = build_section_lines(header_line, leading_blank=False)
              lines = lines[:start] + new_lines + lines[end:]
            else:
              print("CHANGELOG entry exists and appears edited; leaving it unchanged.")
          else:
            insert_at = 1 if lines and lines[0].strip() == "# CHANGELOG" else 0
            new_lines = build_section_lines(default_header, leading_blank=True)
            lines = lines[:insert_at] + new_lines + lines[insert_at:]

          changelog_path.write_text("\\n".join(lines).rstrip() + "\\n")

          print(f"Updated version to {version} (last tag: {last_tag or 'none'})")
          PY
WORKFLOW

  cat <<WORKFLOW

      - name: Install radp-bash-framework
        run: |
          set -euo pipefail
          curl -fsSL https://raw.githubusercontent.com/xooooooooox/radp-bash-framework/main/install.sh | \\
            RADP_BF_INSTALL_MODE=manual bash
          echo "\$HOME/.local/bin" >> "\$GITHUB_PATH"

      - name: Regenerate completion scripts
        run: |
          set -euo pipefail
          export PATH="\$HOME/.local/bin:\$PATH"
          ./bin/${project_name} completion bash > completions/${project_name}.bash
          ./bin/${project_name} completion zsh > completions/${project_name}.zsh
          echo "Completion scripts regenerated"

      - name: Commit changes
        id: commit
        run: |
          set -euo pipefail
          if git diff --quiet; then
            if [[ "\${{ steps.branch.outputs.reused }}" == "true" ]]; then
              echo "No changes to commit; branch already exists."
              exit 0
            fi
            echo "No changes to commit; aborting release prep." >&2
            exit 1
          fi
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add \\
            src/main/shell/commands/version.sh \\
            packaging/copr/${project_name}.spec \\
            packaging/obs/${project_name}.spec \\
            completions/${project_name}.bash \\
            completions/${project_name}.zsh \\
            CHANGELOG.md
          git commit -m "Release prep \${{ steps.version.outputs.version }}"

      - name: Push branch
        run: |
          set -euo pipefail
          git push --set-upstream origin "\${{ steps.branch.outputs.branch }}"

      - name: Create pull request
        env:
          GH_TOKEN: \${{ secrets.RELEASE_PREP_TOKEN || github.token }}
          HAS_RELEASE_PREP_TOKEN: \${{ secrets.RELEASE_PREP_TOKEN != '' }}
        run: |
          set -euo pipefail
          version="\${{ steps.version.outputs.version }}"
          branch="\${{ steps.branch.outputs.branch }}"
          title="Release \${version}"
          body_file="\$(mktemp)"
          cat > "\${body_file}" <<EOF
          Release prep for \${version}.

          - Update gr_app_version in version.sh
          - Sync spec versions
          - Add changelog entry (please review and edit)

          When this PR is merged, create-version-tag will run automatically to validate and tag the release.
          EOF
          if gh pr create --base main --head "\${branch}" --title "\${title}" --body-file "\${body_file}"; then
            exit 0
          fi
          if gh pr view --head "\${branch}" >/dev/null 2>&1; then
            echo "PR already exists for \${branch}."
            exit 0
          fi
          if [[ "\${HAS_RELEASE_PREP_TOKEN}" == "true" ]]; then
            echo "PR creation failed even with RELEASE_PREP_TOKEN; check token scopes and repo settings." >&2
            exit 1
          fi
          echo "PR creation not permitted for GitHub Actions token." >&2
          echo "Enable 'Allow GitHub Actions to create and approve pull requests' or create the PR manually:" >&2
          echo "  gh pr create --base main --head \"\${branch}\" --title \"\${title}\" --body-file \"\${body_file}\"" >&2
WORKFLOW
}

radp_workflow_content_create_tag() {
  local project_name="$1"
  local project_var="$2"

  cat <<'WORKFLOW'
name: Create version tag

on:
  workflow_dispatch:
  pull_request:
    types:
      - closed
    branches:
      - main

permissions:
  contents: write

jobs:
  create-version-tag:
    if: >-
      ${{
        (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
        (github.event_name == 'pull_request' &&
          github.event.pull_request.merged == true &&
          startsWith(github.event.pull_request.head.ref, 'workflow/v'))
      }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout (manual)
        if: github.event_name == 'workflow_dispatch'
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: main

      - name: Checkout (auto)
        if: github.event_name == 'pull_request'
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.merge_commit_sha }}

      - name: Fetch tags
        run: git fetch --tags --force

      - name: Resolve context
        id: ctx
        run: |
          set -euo pipefail
          if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
            head_ref="${{ github.event.pull_request.head.ref }}"
            expected_version="${head_ref#workflow/}"
            echo "expected_version=${expected_version}" >> "$GITHUB_OUTPUT"
            echo "tag_target=${{ github.event.pull_request.merge_commit_sha }}" >> "$GITHUB_OUTPUT"
          else
            echo "expected_version=" >> "$GITHUB_OUTPUT"
            echo "tag_target=HEAD" >> "$GITHUB_OUTPUT"
          fi

      - name: Read version
        id: version
        run: |
          set -euo pipefail
          version_file="src/main/shell/commands/version.sh"
          version=$(grep -oP 'declare -gr gr_app_version="\K[^"]+' "$version_file")
          if [[ -z "$version" ]]; then
            echo "Failed to read gr_app_version from $version_file" >&2
            exit 1
          fi
          if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Version '$version' does not match vx.y.z" >&2
            exit 1
          fi
          expected_version="${{ steps.ctx.outputs.expected_version }}"
          if [[ -n "$expected_version" && "$version" != "$expected_version" ]]; then
            echo "Version mismatch: expected ${expected_version}, got ${version}" >&2
            exit 1
          fi
          echo "version=$version" >> "$GITHUB_OUTPUT"

      - name: Validate changelog
        run: |
          set -euo pipefail
          version="${{ steps.version.outputs.version }}"
          version_no_prefix="${version#v}"
          changelog_file="CHANGELOG.md"
          if [[ ! -f "$changelog_file" ]]; then
            echo "CHANGELOG.md not found; update changelog before tagging." >&2
            exit 1
          fi
          if ! grep -Eq "^##[[:space:]]+v?${version_no_prefix}([[:space:]]|$)" "$changelog_file"; then
            echo "CHANGELOG.md missing entry for ${version}; update changelog before tagging." >&2
            exit 1
          fi
WORKFLOW

  cat <<WORKFLOW

      - name: Validate spec versions
        run: |
          set -euo pipefail
          version="\${{ steps.version.outputs.version }}"
          version_no_prefix="\${version#v}"
          copr_spec_file="packaging/copr/${project_name}.spec"
          obs_spec_file="packaging/obs/${project_name}.spec"
          for spec_file in "\$copr_spec_file" "\$obs_spec_file"; do
            if [[ ! -f "\$spec_file" ]]; then
              echo "Spec file not found: \$spec_file" >&2
              exit 1
            fi
            spec_version="\$(awk -F'[: ]+' '/^Version:/{print \$2; exit}' "\$spec_file")"
            if [[ -z "\$spec_version" ]]; then
              echo "Failed to read Version from \$spec_file" >&2
              exit 1
            fi
            if [[ "\$spec_version" != "\$version_no_prefix" ]]; then
              echo "Spec version mismatch in \$spec_file: expected \$version_no_prefix, got \$spec_version" >&2
              echo "Run release-prep to sync spec versions before tagging." >&2
              exit 1
            fi
          done
WORKFLOW

  cat <<'WORKFLOW'

      - name: Create and push tag
        run: |
          set -euo pipefail
          version="${{ steps.version.outputs.version }}"
          tag_target="${{ steps.ctx.outputs.tag_target }}"
          if [[ -z "$tag_target" ]]; then
            tag_target="HEAD"
          fi
          if ! git cat-file -e "${tag_target}^{commit}" 2>/dev/null; then
            echo "Tag target not found: ${tag_target}" >&2
            exit 1
          fi
          if git rev-parse "$version" >/dev/null 2>&1; then
            echo "Tag $version already exists."
            exit 0
          fi
          git tag "$version" "$tag_target"
          git push origin "$version"
WORKFLOW
}

radp_workflow_content_update_spec() {
  local project_name="$1"
  local project_var="$2"

  cat <<'WORKFLOW'
name: Update spec version

on:
  workflow_run:
    workflows:
      - Create version tag
    types:
      - completed
  workflow_dispatch:

permissions:
  contents: write

jobs:
  update-spec-version:
    if: >-
      ${{
        github.event_name != 'workflow_run' ||
        (github.event.workflow_run.conclusion == 'success' &&
        (github.event.workflow_run.head_branch == 'main' ||
         startsWith(github.event.workflow_run.head_branch, 'workflow/')))
      }}
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: main

      - name: Fetch tags
        run: git fetch --tags --force

      - name: Read and validate version
        id: version
        run: |
          set -euo pipefail
          version_file="src/main/shell/commands/version.sh"
          version=$(grep -oP 'declare -gr gr_app_version="\K[^"]+' "$version_file")
          if [[ -z "$version" ]]; then
            echo "Failed to read gr_app_version from $version_file" >&2
            exit 1
          fi
          if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Version '$version' does not match vx.y.z" >&2
            exit 1
          fi
          echo "version=$version" >> "$GITHUB_OUTPUT"
WORKFLOW

  cat <<WORKFLOW

      - name: Update spec version
        run: |
          set -euo pipefail
          version="\${{ steps.version.outputs.version }}"
          version_no_prefix="\${version#v}"
          copr_spec_file="packaging/copr/${project_name}.spec"
          if [[ -f "\$copr_spec_file" ]]; then
            sed -i "s/^Version:.*/Version:        \${version_no_prefix}/" "\$copr_spec_file"
          else
            echo "COPR spec file not found: \$copr_spec_file" >&2
            exit 1
          fi
          obs_spec_file="packaging/obs/${project_name}.spec"
          if [[ -f "\$obs_spec_file" ]]; then
            sed -i "s/^Version:.*/Version:        \${version_no_prefix}/" "\$obs_spec_file"
          else
            echo "OBS spec file not found: \$obs_spec_file" >&2
            exit 1
          fi
WORKFLOW

  cat <<WORKFLOW

      - name: Commit and push
        run: |
          set -euo pipefail
          if git diff --quiet; then
            echo "No changes to commit."
            exit 0
          fi
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add packaging/copr/${project_name}.spec
          if [[ -f "packaging/obs/${project_name}.spec" ]]; then
            git add packaging/obs/${project_name}.spec
          fi
          git commit -m "Update spec version to \${{ steps.version.outputs.version }}"
          git push
WORKFLOW
}

radp_workflow_content_build_copr() {
  local project_name="$1"
  local project_var="$2"

  cat <<'WORKFLOW'
name: Build COPR package

on:
  workflow_run:
    workflows:
      - Update spec version
    types:
      - completed

permissions:
  contents: read

jobs:
  build-copr-package:
    if: >-
      ${{
        github.event.workflow_run.conclusion == 'success' &&
        (github.event.workflow_run.head_branch == 'main' ||
         startsWith(github.event.workflow_run.head_branch, 'workflow/'))
      }}
    runs-on: ubuntu-latest
    env:
      COPR_LOGIN: ${{ secrets.COPR_LOGIN }}
      COPR_TOKEN: ${{ secrets.COPR_TOKEN }}
      COPR_USERNAME: ${{ secrets.COPR_USERNAME }}
      COPR_PROJECT: ${{ secrets.COPR_PROJECT }}
    steps:
      - name: Validate COPR secrets
        run: |
          set -euo pipefail
          for value in COPR_LOGIN COPR_TOKEN COPR_USERNAME COPR_PROJECT; do
            if [[ -z "${!value:-}" ]]; then
              echo "Missing required secret: ${value}" >&2
              exit 1
            fi
          done

      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Read version
        id: version
        run: |
          set -euo pipefail
          version_file="src/main/shell/commands/version.sh"
          version=$(grep -oP 'declare -gr gr_app_version="\K[^"]+' "$version_file")
          if [[ -z "$version" ]]; then
            echo "Failed to read gr_app_version from $version_file" >&2
            exit 1
          fi
          echo "version=${version}" >> "$GITHUB_OUTPUT"

      - name: Check tag exists
        id: tag
        run: |
          set -euo pipefail
          version="${{ steps.version.outputs.version }}"
          git fetch --tags --force
          if ! git rev-parse "$version" >/dev/null 2>&1; then
            echo "Tag $version does not exist. Skipping COPR build."
            echo "should_build=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          tag_sha="$(git rev-parse "$version^{commit}")"
          run_sha="${{ github.event.workflow_run.head_sha }}"
          if [[ -n "${run_sha}" && "${tag_sha}" != "${run_sha}" ]]; then
            echo "Tag ${version} points to ${tag_sha}, workflow head is ${run_sha}; skipping COPR build."
            echo "should_build=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          echo "should_build=true" >> "$GITHUB_OUTPUT"
          echo "tag_sha=${tag_sha}" >> "$GITHUB_OUTPUT"

      - name: Install copr-cli
        run: |
          set -euo pipefail
          python -m pip install --upgrade pip
          python -m pip install copr-cli

      - name: Configure copr-cli
        run: |
          set -euo pipefail
          mkdir -p ~/.config
          cat <<CONFIG > ~/.config/copr
          [copr-cli]
          login = ${COPR_LOGIN}
          token = ${COPR_TOKEN}
          username = ${COPR_USERNAME}
          copr_url = https://copr.fedorainfracloud.org
          encrypted = false

          [main]
          login = ${COPR_LOGIN}
          token = ${COPR_TOKEN}
          username = ${COPR_USERNAME}
          copr_url = https://copr.fedorainfracloud.org
          encrypted = false
          CONFIG
WORKFLOW

  cat <<WORKFLOW

      - name: Trigger COPR build
        if: steps.tag.outputs.should_build == 'true'
        run: |
          set -euo pipefail
          copr-cli buildscm "\${COPR_PROJECT}" \\
            --clone-url "\${GITHUB_SERVER_URL}/\${GITHUB_REPOSITORY}.git" \\
            --commit "\${{ steps.tag.outputs.tag_sha }}" \\
            --subdir "packaging/copr" \\
            --spec "${project_name}.spec"
WORKFLOW
}

radp_workflow_content_build_obs() {
  local project_name="$1"
  local project_var="$2"

  cat <<'WORKFLOW'
name: Build OBS package

on:
  workflow_run:
    workflows:
      - Update spec version
    types:
      - completed
  workflow_dispatch:

permissions:
  contents: read

jobs:
  build-obs-package:
    if: >-
      ${{
        github.event_name != 'workflow_run' ||
        (github.event.workflow_run.conclusion == 'success' &&
        (github.event.workflow_run.head_branch == 'main' ||
         startsWith(github.event.workflow_run.head_branch, 'workflow/')))
      }}
    runs-on: ubuntu-latest
    env:
      OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
      OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
      OBS_PROJECT: ${{ secrets.OBS_PROJECT }}
      OBS_PACKAGE: ${{ secrets.OBS_PACKAGE }}
      OBS_API_URL: ${{ secrets.OBS_API_URL }}
    steps:
      - name: Validate OBS secrets
        run: |
          set -euo pipefail
          for value in OBS_USERNAME OBS_PASSWORD OBS_PROJECT OBS_PACKAGE; do
            if [[ -z "${!value:-}" ]]; then
              echo "Missing required secret: ${value}" >&2
              exit 1
            fi
          done

          default_api_url="https://api.opensuse.org"
          if [[ -z "${OBS_API_URL:-}" ]]; then
            echo "OBS_API_URL not set; using default ${default_api_url}"
            echo "OBS_API_URL=${default_api_url}" >> "$GITHUB_ENV"
          else
            echo "OBS_API_URL=${OBS_API_URL}" >> "$GITHUB_ENV"
          fi

      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Fetch tags
        run: git fetch --tags --force

      - name: Read version
        id: version
        run: |
          set -euo pipefail
          version_file="src/main/shell/commands/version.sh"
          version=$(grep -oP 'declare -gr gr_app_version="\K[^"]+' "$version_file")
          if [[ -z "$version" ]]; then
            echo "Failed to read gr_app_version from $version_file" >&2
            exit 1
          fi
          if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Version '$version' does not match vx.y.z" >&2
            exit 1
          fi
          version_no_prefix="${version#v}"
          echo "version=${version}" >> "$GITHUB_OUTPUT"
          echo "version_no_prefix=${version_no_prefix}" >> "$GITHUB_OUTPUT"

      - name: Check tag exists
        id: tag
        run: |
          set -euo pipefail
          version="${{ steps.version.outputs.version }}"
          if ! git rev-parse "$version" >/dev/null 2>&1; then
            echo "Tag $version does not exist. Skipping OBS build."
            echo "should_build=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          tag_sha="$(git rev-parse "$version^{commit}")"
          if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
            run_sha="${{ github.event.workflow_run.head_sha }}"
            if [[ -n "${run_sha}" && "${tag_sha}" != "${run_sha}" ]]; then
              echo "Tag ${version} points to ${tag_sha}, workflow head is ${run_sha}; skipping OBS build."
              echo "should_build=false" >> "$GITHUB_OUTPUT"
              exit 0
            fi
          fi
          echo "should_build=true" >> "$GITHUB_OUTPUT"
          echo "tag_sha=${tag_sha}" >> "$GITHUB_OUTPUT"

      - name: Install osc
        if: steps.tag.outputs.should_build == 'true'
        run: |
          set -euo pipefail
          sudo apt-get update
          sudo apt-get install -y osc dpkg-dev debhelper

      - name: Configure osc
        if: steps.tag.outputs.should_build == 'true'
        run: |
          set -euo pipefail
          cat > ~/.oscrc <<EOF
          [general]
          apiurl = ${OBS_API_URL}

          [${OBS_API_URL}]
          user = ${OBS_USERNAME}
          pass = ${OBS_PASSWORD}
          EOF
          chmod 600 ~/.oscrc

      - name: Verify OBS authentication
        if: steps.tag.outputs.should_build == 'true'
        run: |
          set -euo pipefail
          osc -A "${OBS_API_URL}" ls "${OBS_PROJECT}" >/dev/null
          osc -A "${OBS_API_URL}" ls "${OBS_PROJECT}" "${OBS_PACKAGE}" >/dev/null

      - name: Ensure OBS project config (Preinstall for RHEL_7)
        if: steps.tag.outputs.should_build == 'true'
        run: |
          set -euo pipefail
          current_conf="$(osc -A "${OBS_API_URL}" meta prjconf "${OBS_PROJECT}")"
          if echo "$current_conf" | grep -q 'Preinstall:.*perl-Digest-MD5'; then
            echo "perl-Digest-MD5 already in Preinstall; skipping."
            exit 0
          fi
          {
            echo "$current_conf"
            echo ""
            echo '%if "%_repository" == "RHEL_7"'
            echo "Preinstall: perl-Digest-MD5"
            echo "%endif"
          } | osc -A "${OBS_API_URL}" meta prjconf "${OBS_PROJECT}" -F -
          echo "Added perl-Digest-MD5 to Preinstall for RHEL_7."
WORKFLOW

  cat <<WORKFLOW

      - name: Sync sources to OBS
        if: steps.tag.outputs.should_build == 'true'
        run: |
          set -euo pipefail
          version="\${{ steps.version.outputs.version }}"
          version_no_prefix="\${{ steps.version.outputs.version_no_prefix }}"
          checkout_root="\${{ runner.temp }}/obs"
          mkdir -p "\$checkout_root"
          pushd "\$checkout_root" >/dev/null

          osc -A "\${OBS_API_URL}" checkout "\${OBS_PROJECT}" "\${OBS_PACKAGE}"
          pkg_dir="\${OBS_PROJECT}/\${OBS_PACKAGE}"

          if [[ ! -d "\$pkg_dir/.osc" ]]; then
            echo "OBS checkout missing .osc directory at \$pkg_dir" >&2
            exit 1
          fi

          shopt -s dotglob
          for entry in "\$pkg_dir"/*; do
            [[ "\$(basename "\$entry")" == ".osc" ]] && continue
            rm -rf "\$entry"
          done
          shopt -u dotglob

          spec_source="\${GITHUB_WORKSPACE}/packaging/obs/${project_name}.spec"
          if [[ ! -f "\$spec_source" ]]; then
            echo "Spec file not found at \$spec_source" >&2
            exit 1
          fi
          cp "\$spec_source" "\$pkg_dir/"

          tarball_url="\${GITHUB_SERVER_URL}/\${GITHUB_REPOSITORY}/archive/refs/tags/\${version}.tar.gz"
          tarball_name="v\${version_no_prefix}.tar.gz"
          tarball_path="\$pkg_dir/\${tarball_name}"
          curl -L --fail --show-error -o "\$tarball_path" "\$tarball_url"
          if [[ ! -s "\$tarball_path" ]]; then
            echo "Failed to download tarball from \$tarball_url" >&2
            exit 1
          fi

          if [[ -d "\${GITHUB_WORKSPACE}/packaging/obs/debian" ]]; then
            cp -a "\${GITHUB_WORKSPACE}/packaging/obs/debian" "\$pkg_dir/debian"
          elif [[ -d "\${GITHUB_WORKSPACE}/packaging/deb/debian" ]]; then
            cp -a "\${GITHUB_WORKSPACE}/packaging/deb/debian" "\$pkg_dir/debian"
          else
            echo "No debian/ metadata found; skipping debian sync."
          fi

          if [[ -d "\$pkg_dir/debian" ]]; then
            deb_version="\${version_no_prefix}-1"
            changelog_path="\$pkg_dir/debian/changelog"
            {
              printf '${project_name} (%s) unstable; urgency=medium\\n\\n' "\$deb_version"
              printf '  * Automated OBS build for version %s\\n\\n' "\$deb_version"
              printf ' -- xooooooooox <xozoz.sos@gmail.com>  %s\\n' "\$(date -R)"
            } > "\$changelog_path"
            tmp_build="\$(mktemp -d)"
            orig_tarball="\$tmp_build/${project_name}_\${version_no_prefix}.orig.tar.gz"
            cp "\$tarball_path" "\$orig_tarball"
            tar --force-local -xzf "\$orig_tarball" -C "\$tmp_build"
            src_dir="\$tmp_build/${project_name}-\${version_no_prefix}"
            if [[ ! -d "\$src_dir" ]]; then
              echo "Expected source dir not found: \$src_dir" >&2
              exit 1
            fi
            cp -a "\$pkg_dir/debian" "\$src_dir/debian"
            (cd "\$src_dir" && dpkg-source -b .)
            find "\$tmp_build" -maxdepth 1 -type f -name '${project_name}_*.dsc' -print0 | xargs -0 -I {} mv {} "\$pkg_dir/"
            find "\$tmp_build" -maxdepth 1 -type f -name '${project_name}_*.debian.tar.*' -print0 | xargs -0 -I {} mv {} "\$pkg_dir/"
            cp "\$orig_tarball" "\$pkg_dir/"
            rm -rf "\$tmp_build"
          fi

          pushd "\$pkg_dir" >/dev/null
          osc addremove
          popd >/dev/null
          popd >/dev/null

      - name: Commit and trigger OBS build
        if: steps.tag.outputs.should_build == 'true'
        run: |
          set -euo pipefail
          pkg_dir="\${{ runner.temp }}/obs/\${OBS_PROJECT}/\${OBS_PACKAGE}"
          if [[ ! -d "\$pkg_dir/.osc" ]]; then
            echo "OBS package directory missing at \$pkg_dir" >&2
            exit 1
          fi

          pushd "\$pkg_dir" >/dev/null
          status_output="\$(osc status)"
          if [[ -z "\$status_output" ]]; then
            echo "No OBS changes to commit."
            exit 0
          fi

          osc commit -m "Update ${project_name} to \${{ steps.version.outputs.version }}"
          popd >/dev/null
WORKFLOW
}

radp_workflow_content_homebrew() {
  local project_name="$1"
  local project_var="$2"

  cat <<WORKFLOW
name: Update Homebrew Tap

on:
  push:
    tags:
      - "v*"
  workflow_run:
    workflows:
      - Create version tag
    types:
      - completed
  workflow_dispatch:

jobs:
  update-tap:
    if: >-
      \${{
        github.event_name != 'workflow_run' ||
        (github.event.workflow_run.conclusion == 'success' &&
        (github.event.workflow_run.head_branch == 'main' ||
         startsWith(github.event.workflow_run.head_branch, 'workflow/')))
      }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    env:
      TAP_REPO: xooooooooox/homebrew-radp
      TAP_FORMULA_PATH: Formula/${project_name}.rb
    steps:
      - name: Checkout source
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Fetch tags
        run: git fetch --tags --force

      - name: Checkout tap repository
        uses: actions/checkout@v4
        with:
          repository: \${{ env.TAP_REPO }}
          path: homebrew-radp
          token: \${{ secrets.HOMEBREW_TAP_TOKEN }}

      - name: Prepare release metadata
        id: release
        run: |
          set -euo pipefail
          if [[ "\${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
            head_sha="\${{ github.event.workflow_run.head_sha }}"
            head_branch="\${{ github.event.workflow_run.head_branch }}"
            if [[ "\${head_branch}" == workflow/v* ]]; then
              tag_name="\${head_branch#workflow/}"
            else
              tag_name="\$(git tag --points-at "\${head_sha}" --list 'v*' | sort -V | tail -n 1)"
            fi
          else
            tag_name="\${GITHUB_REF_NAME}"
          fi
          if [[ -z "\${tag_name}" ]]; then
            echo "Failed to resolve version tag for event \${GITHUB_EVENT_NAME}" >&2
            exit 1
          fi
          if ! git rev-parse "\${tag_name}" >/dev/null 2>&1; then
            echo "Resolved tag \${tag_name} does not exist in repository." >&2
            exit 1
          fi
          version_file="src/main/shell/commands/version.sh"
          version_from_tag="\$(git show "\${tag_name}:\${version_file}" | grep -oP 'declare -gr gr_app_version="\\K[^"]+')"
          if [[ -z "\${version_from_tag}" ]]; then
            echo "Failed to read gr_app_version from \${version_file} at \${tag_name}" >&2
            exit 1
          fi
          if [[ "\${version_from_tag}" != "\${tag_name}" ]]; then
            echo "Tag \${tag_name} does not match gr_app_version (\${version_from_tag})" >&2
            exit 1
          fi
          version="\${tag_name#v}"
          tarball_url="https://github.com/\${GITHUB_REPOSITORY}/archive/refs/tags/\${tag_name}.tar.gz"
          sha256="\$(curl -L "\${tarball_url}" | sha256sum | awk '{print \$1}')"
          echo "tag_name=\${tag_name}" >> "\$GITHUB_OUTPUT"
          echo "version=\${version}" >> "\$GITHUB_OUTPUT"
          echo "tarball_url=\${tarball_url}" >> "\$GITHUB_OUTPUT"
          echo "sha256=\${sha256}" >> "\$GITHUB_OUTPUT"

      - name: Create or update formula
        env:
          VERSION: \${{ steps.release.outputs.version }}
          TARBALL_URL: \${{ steps.release.outputs.tarball_url }}
          SHA256: \${{ steps.release.outputs.sha256 }}
        run: |
          set -euo pipefail
          template="packaging/homebrew/${project_name}.rb"
          formula="homebrew-radp/\${TAP_FORMULA_PATH}"
          mkdir -p "\$(dirname "\${formula}")"

          # Copy template and replace placeholders
          cp "\${template}" "\${formula}"
          sed -i "s|%%TARBALL_URL%%|\${TARBALL_URL}|g" "\${formula}"
          sed -i "s|%%SHA256%%|\${SHA256}|g" "\${formula}"
          sed -i "s|%%VERSION%%|\${VERSION}|g" "\${formula}"

      - name: Commit and push tap
        run: |
          set -euo pipefail
          cd homebrew-radp
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add "\${TAP_FORMULA_PATH}" || true
          if git diff --cached --quiet; then
            echo "No changes to commit."
            exit 0
          fi
          git commit -m "Update ${project_name} to \${{ steps.release.outputs.tag_name }}"
          git push
WORKFLOW
}

radp_workflow_content_attach_packages() {
  local project_name="$1"
  local project_var="$2"

  cat <<WORKFLOW
name: Attach release packages

on:
  workflow_run:
    workflows:
      - Build COPR package
      - Build OBS package
      - Update Homebrew Tap
    types:
      - completed
  workflow_dispatch:

permissions:
  contents: write

jobs:
  attach-release-packages:
    if: >-
      \${{
        github.event_name != 'workflow_run' ||
        (github.event.workflow_run.conclusion == 'success' &&
        (github.event.workflow_run.head_branch == 'main' ||
         startsWith(github.event.workflow_run.head_branch, 'workflow/')))
      }}
    runs-on: ubuntu-latest
    env:
      COPR_PROJECT: \${{ secrets.COPR_PROJECT }}
      OBS_PROJECT: \${{ secrets.OBS_PROJECT }}
      OBS_PACKAGE: \${{ secrets.OBS_PACKAGE }}
      OBS_API_URL: \${{ secrets.OBS_API_URL }}
      OBS_USERNAME: \${{ secrets.OBS_USERNAME }}
      OBS_PASSWORD: \${{ secrets.OBS_PASSWORD }}
      TAP_FORMULA_URL: https://raw.githubusercontent.com/xooooooooox/homebrew-radp/HEAD/Formula/${project_name}.rb
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Fetch tags
        run: git fetch --tags --force

      - name: Resolve tag and package
        id: release
        shell: bash
        run: |
          set -euo pipefail
          if [[ "\${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
            head_sha="\${{ github.event.workflow_run.head_sha }}"
            tag_name="\$(git tag --points-at "\${head_sha}" --list 'v*' | sort -V | tail -n 1)"
          else
            tag_name="\${GITHUB_REF_NAME}"
          fi

          version_file="src/main/shell/commands/version.sh"
          if [[ -z "\${tag_name}" && "\${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
            version_from_commit="\$(git show "\${head_sha}:\${version_file}" | grep -oP 'declare -gr gr_app_version="\\K[^"]+')"
            if [[ -z "\${version_from_commit}" ]]; then
              echo "Failed to read gr_app_version from \${version_file} at \${head_sha}" >&2
              exit 1
            fi
            tag_name="\${version_from_commit}"
          fi

          if [[ -z "\${tag_name}" ]]; then
            echo "Failed to resolve version tag for event \${GITHUB_EVENT_NAME}" >&2
            exit 1
          fi

          if [[ ! "\${tag_name}" =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+\$ ]]; then
            echo "Resolved tag '\${tag_name}' does not match vx.y.z" >&2
            exit 1
          fi

          should_upload=true
          if ! git rev-parse "\${tag_name}" >/dev/null 2>&1; then
            echo "Tag \${tag_name} does not exist yet; skipping release attachment."
            should_upload=false
          elif [[ "\${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
            tag_sha="\$(git rev-parse "\${tag_name}^{commit}")"
            if [[ -n "\${head_sha}" && "\${tag_sha}" != "\${head_sha}" ]]; then
              echo "Tag \${tag_name} points to \${tag_sha}, workflow head is \${head_sha}; skipping release attachment."
              should_upload=false
            fi
          fi

          version="\${tag_name#v}"
          package_name="\$(awk -F': *' '/^Name:/{print \$2; exit}' packaging/copr/${project_name}.spec)"
          if [[ -z "\${package_name}" ]]; then
            echo "Failed to read package name from packaging/copr/${project_name}.spec" >&2
            exit 1
          fi

          echo "tag_name=\${tag_name}" >> "\$GITHUB_OUTPUT"
          echo "version=\${version}" >> "\$GITHUB_OUTPUT"
          echo "package_name=\${package_name}" >> "\$GITHUB_OUTPUT"
          echo "should_upload=\${should_upload}" >> "\$GITHUB_OUTPUT"

      - name: Download COPR packages
        if: >-
          \${{
            steps.release.outputs.should_upload == 'true' &&
            (github.event_name != 'workflow_run' ||
            github.event.workflow_run.name == 'Build COPR package')
          }}
        env:
          VERSION: \${{ steps.release.outputs.version }}
          PACKAGE_NAME: \${{ steps.release.outputs.package_name }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ -z "\${COPR_PROJECT:-}" ]]; then
            echo "COPR_PROJECT not set; skipping COPR download."
            exit 0
          fi
          python - <<'PY'
          import json
          import os
          import re
          import sys
          import urllib.parse
          import urllib.request
          from pathlib import Path

          copr_project = os.environ["COPR_PROJECT"].strip()
          version = os.environ["VERSION"].strip()
          package_name = os.environ["PACKAGE_NAME"].strip()

          if "/" not in copr_project:
            print(f"Invalid COPR_PROJECT: {copr_project}", file=sys.stderr)
            sys.exit(0)

          owner, project = copr_project.split("/", 1)

          def request(url: str) -> urllib.request.Request:
            return urllib.request.Request(
              url,
              headers={
                "User-Agent": "${project_name}-release-bot/1.0",
                "Accept": "application/json,text/html;q=0.9,*/*;q=0.8",
              },
            )

          def fetch(url: str) -> tuple[bytes, int, str]:
            req = request(url)
            with urllib.request.urlopen(req) as resp:
              data = resp.read()
              status = getattr(resp, "status", None) or resp.getcode()
              content_type = resp.headers.get("Content-Type", "")
              return data, int(status), content_type

          def api_json(url: str) -> dict:
            data, status, content_type = fetch(url)
            if not data:
              raise ValueError(f"Empty response from {url}")
            try:
              return json.loads(data.decode("utf-8"))
            except json.JSONDecodeError as exc:
              snippet = data[:200].decode("utf-8", "ignore")
              raise ValueError(f"Invalid JSON from {url}: {snippet}") from exc

          def download(url: str, dest: Path) -> None:
            dest.parent.mkdir(parents=True, exist_ok=True)
            data, status, content_type = fetch(url)
            if status >= 400:
              raise ValueError(f"Download failed: {url} status={status}")
            with open(dest, "wb") as fh:
              fh.write(data)

          params = {
            "ownername": owner,
            "projectname": project,
            "limit": "50",
          }
          builds_url = "https://copr.fedorainfracloud.org/api_3/build/list?" + urllib.parse.urlencode(params)
          try:
            build_data = api_json(builds_url)
            builds = build_data.get("builds") or build_data.get("items") or []
          except Exception as exc:
            print(f"Failed to read COPR build list: {exc}", file=sys.stderr)
            builds = []

          candidates = []
          for build in builds:
            if build.get("state") != "succeeded":
              continue
            source_pkg = build.get("source_package") or {}
            source_name = source_pkg.get("name") or build.get("package_name")
            if source_name and source_name != package_name:
              continue
            src_version = source_pkg.get("version")
            if src_version and not (src_version == version or src_version.startswith(version + "-")):
              continue
            candidates.append(build)

          if not candidates:
            print(f"No COPR builds found for {package_name} {version}", file=sys.stderr)
            sys.exit(0)

          selected = max(candidates, key=lambda b: b.get("id", 0))
          build_id = selected.get("id")
          repo_url = (selected.get("repo_url") or "").rstrip("/")
          chroots = selected.get("chroots") or []

          dest_dir = Path("release-assets/copr")
          downloaded = set()

          def fetch_listing(url: str) -> str:
            data, status, content_type = fetch(url)
            return data.decode("utf-8", "ignore")

          def download_rpms_from_listing(base_url: str) -> int:
            try:
              listing = fetch_listing(base_url)
            except Exception:
              return 0
            hrefs = re.findall(r'href=["\']([^"\']+)["\']', listing)
            count = 0
            for href in hrefs:
              if href in ("../", "./"):
                continue
              href_clean = href.split("?", 1)[0].split("#", 1)[0]
              if not href_clean.endswith(".rpm"):
                continue
              filename = href_clean.split("/")[-1]
              if filename.endswith(".src.rpm"):
                continue
              if package_name not in filename or version not in filename:
                continue
              if filename in downloaded:
                continue
              full_url = urllib.parse.urljoin(base_url, href_clean)
              try:
                download(full_url, dest_dir / f"copr-{filename}")
              except Exception:
                continue
              downloaded.add(filename)
              count += 1
            return count

          if build_id and package_name and chroots:
            build_dir_candidates = [f"{int(build_id):08d}-{package_name}", f"{build_id}-{package_name}"]
            for chroot in chroots:
              chroot_url = f"{repo_url}/{chroot}/"
              for build_dir in build_dir_candidates:
                direct_url = f"{chroot_url}{build_dir}/"
                download_rpms_from_listing(direct_url)

          if downloaded:
            print(f"Downloaded {len(downloaded)} COPR RPMs", file=sys.stderr)
          else:
            print(f"No COPR RPMs downloaded for {package_name} {version}", file=sys.stderr)
          PY

      - name: Download OBS packages
        if: >-
          \${{
            steps.release.outputs.should_upload == 'true' &&
            (github.event_name != 'workflow_run' ||
            github.event.workflow_run.name == 'Build OBS package')
          }}
        env:
          VERSION: \${{ steps.release.outputs.version }}
          PACKAGE_NAME: \${{ steps.release.outputs.package_name }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ -z "\${OBS_PROJECT:-}" ]]; then
            echo "OBS_PROJECT not set; skipping OBS download."
            exit 0
          fi
          python - <<'PY'
          import os
          import sys
          import base64
          import urllib.error
          import urllib.parse
          import urllib.request
          import xml.etree.ElementTree as ET
          from pathlib import Path
          import time

          project = os.environ["OBS_PROJECT"].strip()
          package = (os.environ.get("OBS_PACKAGE") or os.environ.get("PACKAGE_NAME") or "").strip()
          version = os.environ["VERSION"].strip()
          api_url = (os.environ.get("OBS_API_URL") or "https://api.opensuse.org").rstrip("/")
          username = (os.environ.get("OBS_USERNAME") or "").strip()
          password = (os.environ.get("OBS_PASSWORD") or "").strip()
          max_wait_seconds = int(os.environ.get("OBS_WAIT_SECONDS", "1200"))
          poll_interval = int(os.environ.get("OBS_POLL_INTERVAL", "30"))

          if not package:
            print("OBS package name is missing", file=sys.stderr)
            sys.exit(0)

          def quote(segment: str) -> str:
            return urllib.parse.quote(segment, safe="")

          def request(url: str) -> urllib.request.Request:
            req = urllib.request.Request(
              url,
              headers={
                "User-Agent": "${project_name}-release-bot/1.0",
                "Accept": "application/xml,text/xml;q=0.9,*/*;q=0.8",
              },
            )
            if username and password:
              token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")
              req.add_header("Authorization", f"Basic {token}")
            return req

          def fetch(url: str) -> tuple[bytes, int, str]:
            req = request(url)
            try:
              with urllib.request.urlopen(req) as resp:
                data = resp.read()
                status = getattr(resp, "status", None) or resp.getcode()
                content_type = resp.headers.get("Content-Type", "")
                return data, int(status), content_type
            except urllib.error.HTTPError as exc:
              raise ValueError(f"HTTP {exc.code} for {url}") from exc

          def fetch_xml(url: str) -> ET.Element:
            data, status, content_type = fetch(url)
            if not data:
              raise ValueError(f"Empty response from {url}")
            return ET.fromstring(data)

          def download(url: str, dest: Path) -> None:
            dest.parent.mkdir(parents=True, exist_ok=True)
            with urllib.request.urlopen(request(url)) as resp, open(dest, "wb") as fh:
              fh.write(resp.read())

          project_q = quote(project)
          package_q = quote(package)
          result_url = f"{api_url}/build/{project_q}/_result?package={package_q}"

          dest_dir = Path("release-assets/obs")
          downloaded: set[str] = set()
          deadline = time.time() + max_wait_seconds

          while True:
            try:
              root = fetch_xml(result_url)
            except Exception as exc:
              print(f"Failed to query OBS results: {exc}", file=sys.stderr)
              break

            candidates = []
            for result in root.findall(".//result"):
              repo = result.get("repository")
              arch = result.get("arch")
              if not repo or not arch:
                continue
              status_nodes = result.findall(f"./status[@package='{package}']")
              if not status_nodes:
                continue
              code = (status_nodes[0].get("code") or "").lower()
              if code in {"succeeded", "published", "finished", "unchanged"}:
                candidates.append((repo, arch))

            for repo, arch in candidates:
              repo_q = quote(repo)
              arch_q = quote(arch)
              base_url = f"{api_url}/build/{project_q}/{repo_q}/{arch_q}/{package_q}"
              try:
                binaries_root = fetch_xml(base_url)
              except Exception:
                continue
              for binary in binaries_root.findall(".//binary"):
                filename = binary.get("filename")
                if not filename:
                  continue
                lower = filename.lower()
                if lower.endswith((".src.rpm", ".nosrc.rpm", ".ddeb")):
                  continue
                if not (lower.endswith(".rpm") or lower.endswith(".deb")):
                  continue
                if version not in filename:
                  continue
                if filename in downloaded:
                  continue
                file_url = f"{base_url}/{urllib.parse.quote(filename, safe='')}"
                try:
                  download(file_url, dest_dir / f"obs-{filename}")
                except Exception:
                  continue
                downloaded.add(filename)

            if downloaded:
              break

            if time.time() + poll_interval > deadline:
              break

            print(f"OBS binaries not ready; retrying in {poll_interval}s", file=sys.stderr)
            time.sleep(poll_interval)

          if downloaded:
            print(f"Downloaded {len(downloaded)} OBS binaries")
          else:
            print(f"No OBS binaries downloaded for {package} {version}", file=sys.stderr)
          PY

      - name: Download Homebrew formula
        if: >-
          \${{
            steps.release.outputs.should_upload == 'true' &&
            (github.event_name != 'workflow_run' ||
            github.event.workflow_run.name == 'Update Homebrew Tap')
          }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ -z "\${TAP_FORMULA_URL:-}" ]]; then
            echo "TAP_FORMULA_URL not set; skipping formula download."
            exit 0
          fi
          mkdir -p release-assets/homebrew
          curl -L --fail --show-error -o release-assets/homebrew/homebrew-${project_name}.rb "\${TAP_FORMULA_URL}"

      - name: Upload assets to release
        if: steps.release.outputs.should_upload == 'true'
        env:
          GH_TOKEN: \${{ github.token }}
        shell: bash
        run: |
          set -euo pipefail
          tag="\${{ steps.release.outputs.tag_name }}"
          if ! gh release view "\$tag" >/dev/null 2>&1; then
            gh release create "\$tag" --title "\$tag" --generate-notes
          fi
          if [[ ! -d release-assets ]]; then
            echo "No assets directory to upload."
            exit 0
          fi
          mapfile -t assets < <(find release-assets -maxdepth 3 -type f | sort)
          if [[ \${#assets[@]} -eq 0 ]]; then
            echo "No assets to upload."
            exit 0
          fi
          gh release upload "\$tag" "\${assets[@]}" --clobber
WORKFLOW
}

radp_workflow_content_cleanup_branches() {
  local project_name="$1"
  local project_var="$2"

  cat <<'WORKFLOW'
name: Cleanup stale workflow branches

on:
  schedule:
    # Run every Sunday at 00:00 UTC
    - cron: "0 0 * * 0"
  workflow_dispatch:
    inputs:
      days_old:
        description: "Delete branches older than this many days"
        type: number
        default: 14
        required: false
      dry_run:
        description: "Dry run (list branches without deleting)"
        type: boolean
        default: false
        required: false

permissions:
  contents: write

jobs:
  cleanup-branches:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Delete stale workflow branches
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail

          # Configuration
          days_old="${{ inputs.days_old || 14 }}"
          dry_run="${{ inputs.dry_run || false }}"
          branch_pattern="workflow/v"

          echo "Configuration:"
          echo "  Days old threshold: ${days_old}"
          echo "  Dry run: ${dry_run}"
          echo "  Branch pattern: ${branch_pattern}*"
          echo ""

          # Calculate cutoff date
          cutoff_date=$(date -d "${days_old} days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || \
                        date -v-${days_old}d +%Y-%m-%dT%H:%M:%SZ)
          cutoff_epoch=$(date -d "${cutoff_date}" +%s 2>/dev/null || \
                         date -j -f "%Y-%m-%dT%H:%M:%SZ" "${cutoff_date}" +%s)

          echo "Cutoff date: ${cutoff_date}"
          echo ""

          # Get remote branches matching pattern
          git fetch --prune origin

          deleted_count=0
          skipped_count=0

          while IFS= read -r ref; do
            [[ -z "$ref" ]] && continue

            branch="${ref#refs/remotes/origin/}"
            [[ "$branch" != ${branch_pattern}* ]] && continue

            # Get last commit date for this branch
            last_commit_date=$(git log -1 --format=%cI "origin/${branch}" 2>/dev/null || echo "")
            if [[ -z "$last_commit_date" ]]; then
              echo "[SKIP] ${branch} (no commit date)"
              skipped_count=$((skipped_count + 1))
              continue
            fi

            # Convert to epoch for comparison
            commit_epoch=$(date -d "${last_commit_date}" +%s 2>/dev/null || \
                           date -j -f "%Y-%m-%dT%H:%M:%S%z" "${last_commit_date}" +%s 2>/dev/null || \
                           echo "0")

            if [[ "$commit_epoch" -lt "$cutoff_epoch" ]]; then
              if [[ "$dry_run" == "true" ]]; then
                echo "[DRY-RUN] Would delete: ${branch} (last commit: ${last_commit_date})"
              else
                echo "[DELETE] ${branch} (last commit: ${last_commit_date})"
                git push origin --delete "${branch}" || {
                  echo "  Failed to delete ${branch}"
                  skipped_count=$((skipped_count + 1))
                  continue
                }
              fi
              deleted_count=$((deleted_count + 1))
            else
              echo "[KEEP] ${branch} (last commit: ${last_commit_date})"
              skipped_count=$((skipped_count + 1))
            fi
          done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/)

          echo ""
          echo "Summary:"
          if [[ "$dry_run" == "true" ]]; then
            echo "  Would delete: ${deleted_count} branches"
          else
            echo "  Deleted: ${deleted_count} branches"
          fi
          echo "  Kept/Skipped: ${skipped_count} branches"
WORKFLOW
}

#######################################
# Packaging Content Generators
# These functions generate the content for packaging files.
# Used by both scaffold and upgrade.
#######################################

#######################################
# Generate RPM spec file content
# Arguments:
#   1 - project_name: Project name
#   2 - today: Date string for changelog (e.g., "Sat Jan 25 2026")
# Outputs:
#   Complete spec file content to stdout
#######################################
radp_packaging_content_spec() {
  local project_name="$1"
  local today="$2"

  cat <<SPEC
Name:           ${project_name}
Version:        0.1.0
Release:        1%{?dist}
Summary:        CLI tool built with radp-bash-framework

License:        MIT
URL:            https://github.com/xooooooooox/${project_name}
Source0:        %{url}/archive/refs/tags/v%{version}.tar.gz

BuildArch:      noarch
Requires:       bash
Requires:       coreutils
Requires:       radp-bash-framework

%description
${project_name} is a CLI tool built with radp-bash-framework.

%prep
%setup -q -n ${project_name}-%{version}

%build

%install
rm -rf %{buildroot}
mkdir -p %{buildroot}%{_libdir}/${project_name}
cp -a bin %{buildroot}%{_libdir}/${project_name}/
cp -a src %{buildroot}%{_libdir}/${project_name}/

# Remove IDE support files (development only, not needed at runtime)
find %{buildroot}%{_libdir}/${project_name}/src -name "_ide*.sh" -delete

chmod 0755 %{buildroot}%{_libdir}/${project_name}/bin/${project_name}
find %{buildroot}%{_libdir}/${project_name}/src -type f -name "*.sh" -exec chmod 0755 {} \;
mkdir -p %{buildroot}%{_bindir}
ln -s %{_libdir}/${project_name}/bin/${project_name} %{buildroot}%{_bindir}/${project_name}

# install shell completions
mkdir -p %{buildroot}%{_datadir}/bash-completion/completions
mkdir -p %{buildroot}%{_datadir}/zsh/site-functions
cp -a completions/${project_name}.bash %{buildroot}%{_datadir}/bash-completion/completions/${project_name}
cp -a completions/${project_name}.zsh %{buildroot}%{_datadir}/zsh/site-functions/_${project_name}

%files
%license LICENSE
%doc README.md
%{_bindir}/${project_name}
%{_libdir}/${project_name}/
%{_datadir}/bash-completion/completions/${project_name}
%{_datadir}/zsh/site-functions/_${project_name}

%changelog
* ${today} xooooooooox <xozoz.sos@gmail.com> - 0.1.0-1
- Initial RPM package
SPEC
}

#######################################
# Generate Homebrew formula content
# Arguments:
#   1 - project_name: Project name
# Outputs:
#   Complete formula content to stdout
#######################################
radp_packaging_content_homebrew() {
  local project_name="$1"

  # Convert project name to Ruby class name (kebab-case to PascalCase)
  local class_name=""
  local word
  local IFS='-'
  for word in $project_name; do
    class_name+="${word^}"
  done

  cat <<FORMULA
# Homebrew formula template for ${project_name}
# The CI workflow uses this template and replaces placeholders with actual values.
#
# Placeholders:
#   %%TARBALL_URL%% - GitHub archive URL for the release tag
#   %%SHA256%%      - SHA256 checksum of the tarball
#   %%VERSION%%     - Version number (without 'v' prefix)
#
# Installation:
#   brew tap xooooooooox/radp
#   brew install ${project_name}

class ${class_name} < Formula
  desc "CLI tool built with radp-bash-framework"
  homepage "https://github.com/xooooooooox/${project_name}"
  url "%%TARBALL_URL%%"
  sha256 "%%SHA256%%"
  version "%%VERSION%%"
  license "MIT"

  depends_on "xooooooooox/radp/radp-bash-framework"

  def install
    # Install to libexec, excluding IDE support files
    libexec.install "bin"
    libexec.install "src"

    # Remove IDE support files (development only, not needed at runtime)
    Dir.glob(libexec/"src/**/_ide*.sh").each { |f| rm f }

    # Create wrapper script that sets up paths
    (bin/"${project_name}").write <<~EOS
      #!/bin/bash
      exec "#{libexec}/bin/${project_name}" "\$@"
    EOS

    # Install shell completions
    bash_completion.install "completions/${project_name}.bash" => "${project_name}"
    zsh_completion.install "completions/${project_name}.zsh" => "_${project_name}"
  end

  def caveats
    <<~EOS
      ${project_name} requires radp-bash-framework (installed as dependency).

      For Vagrant integration, also install radp-vagrant-framework:
        brew install radp-vagrant-framework

      Shell Completions:
        Completions are installed to: #{HOMEBREW_PREFIX}/share/zsh/site-functions/

        For standard Zsh setup (recommended):
          # Rebuild completion cache
          rm -f ~/.zcompdump* && compinit
          # Or restart your terminal

        For Zinit users:
          # Option 1: Add Homebrew's site-functions to fpath (before zinit init)
          fpath=(#{HOMEBREW_PREFIX}/share/zsh/site-functions \$fpath)

          # Option 2: Use zinit snippet
          zinit ice as"completion"
          zinit snippet #{HOMEBREW_PREFIX}/share/zsh/site-functions/_${project_name}

        For Oh-My-Zsh users:
          ln -sf #{HOMEBREW_PREFIX}/share/zsh/site-functions/_${project_name} \\
            \${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/${project_name}/_${project_name}

        For Bash:
          brew install bash-completion@2
          # Add to ~/.bash_profile or ~/.bashrc:
          [[ -r "#{HOMEBREW_PREFIX}/etc/profile.d/bash_completion.sh" ]] && \\
            source "#{HOMEBREW_PREFIX}/etc/profile.d/bash_completion.sh"

        Alternative - Dynamic completion (always up-to-date):
          # Bash: eval "\$(${project_name} completion bash)"
          # Zsh:  eval "\$(${project_name} completion zsh)"

      Quick start:
        ${project_name} --help
    EOS
  end

  test do
    # Basic test - check if help works
    system "#{bin}/${project_name}", "--help"
  end
end
FORMULA
}

#######################################
# Generate install.sh content
# Arguments:
#   1 - project_name: Project name
# Outputs:
#   Complete install.sh content to stdout
# Note: Output contains placeholders that need sed replacement
#######################################
radp_packaging_content_install_sh() {
  local project_name="$1"
  local project_var="${project_name//-/_}"
  local project_upper="${project_var^^}"

  cat <<'INSTALL_SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

REPO_OWNER="xooooooooox"
REPO_NAME="__PROJECT_NAME__"
tmp_dir=""

# Defaults (overridable by CLI args, then env vars)
OPT_MODE="${__PROJECT_UPPER___INSTALL_MODE:-auto}"
OPT_REF="${__PROJECT_UPPER___REF:-}"
OPT_VERSION="${__PROJECT_UPPER___VERSION:-}"
OPT_INSTALL_DIR="${__PROJECT_UPPER___INSTALL_DIR:-}"
OPT_BIN_DIR="${__PROJECT_UPPER___BIN_DIR:-}"

# ============================================================================
# Logging
# ============================================================================

log() {
  printf "%s\n" "$*"
}

err() {
  printf "__PROJECT_NAME__ install: %s\n" "$*" >&2
}

die() {
  err "$@"
  exit 1
}

have() {
  command -v "$1" >/dev/null 2>&1
}

# ============================================================================
# Usage
# ============================================================================

usage() {
  cat <<'EOF'
__PROJECT_NAME__ installer

Usage:
  install.sh [OPTIONS]
  curl -fsSL .../install.sh | bash -s -- [OPTIONS]

Options:
  --ref <ref>           Install from a git ref (branch, tag, or SHA).
                        Implies manual installation. If a package-manager
                        version is already installed, it is removed first.
  --mode <mode>         Installation mode (default: auto)
                        auto     - use package manager if available, else manual
                        manual   - always download from GitHub
                        homebrew - force Homebrew
                        dnf      - force dnf
                        yum      - force yum
                        apt      - force apt
                        zypper   - force zypper
  --install-dir <dir>   Manual install location
                        (default: $HOME/.local/lib/__PROJECT_NAME__)
  --bin-dir <dir>       Symlink location (default: $HOME/.local/bin)
  -h, --help            Show this help

Environment variables:
  __PROJECT_UPPER___REF            Same as --ref
  __PROJECT_UPPER___VERSION        Pin to a release version (e.g. v0.1.3)
  __PROJECT_UPPER___INSTALL_MODE   Same as --mode
  __PROJECT_UPPER___INSTALL_DIR    Same as --install-dir
  __PROJECT_UPPER___BIN_DIR        Same as --bin-dir

Examples:
  # Default: auto-detect package manager
  bash install.sh

  # Install latest from main branch (removes pkm version if present)
  bash install.sh --ref main

  # Install a specific tag via manual download
  bash install.sh --ref v0.2.0-rc1

  # Force manual mode (latest release)
  bash install.sh --mode manual
EOF
}

# ============================================================================
# Argument Parsing
# ============================================================================

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
    --ref)
      [[ $# -lt 2 ]] && die "--ref requires a value"
      OPT_REF="$2"
      shift 2
      ;;
    --mode)
      [[ $# -lt 2 ]] && die "--mode requires a value"
      OPT_MODE="$2"
      shift 2
      ;;
    --install-dir)
      [[ $# -lt 2 ]] && die "--install-dir requires a value"
      OPT_INSTALL_DIR="$2"
      shift 2
      ;;
    --bin-dir)
      [[ $# -lt 2 ]] && die "--bin-dir requires a value"
      OPT_BIN_DIR="$2"
      shift 2
      ;;
    -h | --help)
      usage
      exit 0
      ;;
    *)
      die "Unknown option: $1 (use --help for usage)"
      ;;
    esac
  done
}

# ============================================================================
# Package Manager Detection and Installation
# ============================================================================

detect_os() {
  local os=""
  if [[ "${OSTYPE:-}" == darwin* ]]; then
    os="macos"
  elif [[ -f /etc/os-release ]]; then
    # shellcheck disable=SC1091
    source /etc/os-release
    case "${ID:-}" in
    fedora | centos | rhel | rocky | almalinux | ol)
      os="rhel"
      ;;
    debian | ubuntu | linuxmint | pop)
      os="debian"
      ;;
    opensuse* | sles)
      os="suse"
      ;;
    *)
      os="linux"
      ;;
    esac
  else
    os="unknown"
  fi
  echo "${os}"
}

detect_package_manager() {
  local os
  os="$(detect_os)"

  if have brew; then
    echo "homebrew"
    return 0
  fi

  case "${os}" in
  rhel)
    if have dnf; then
      echo "dnf"
      return 0
    elif have yum; then
      echo "yum"
      return 0
    fi
    ;;
  debian)
    if have apt-get; then
      echo "apt"
      return 0
    fi
    ;;
  suse)
    if have zypper; then
      echo "zypper"
      return 0
    fi
    ;;
  esac

  echo ""
}

detect_pkm_installed() {
  if have brew && brew list --formula __PROJECT_NAME__ &>/dev/null; then
    echo "homebrew"
    return 0
  fi

  if have rpm && rpm -q __PROJECT_NAME__ &>/dev/null; then
    if have dnf; then
      echo "dnf"
    elif have yum; then
      echo "yum"
    else
      echo "rpm"
    fi
    return 0
  fi

  if have dpkg && dpkg -s __PROJECT_NAME__ &>/dev/null; then
    echo "apt"
    return 0
  fi

  if have zypper && zypper se -i __PROJECT_NAME__ &>/dev/null; then
    echo "zypper"
    return 0
  fi

  echo ""
}

uninstall_pkm() {
  local pkm="$1"

  log "Removing package-manager version (${pkm}) to avoid conflicts..."

  case "${pkm}" in
  homebrew)
    brew uninstall __PROJECT_NAME__ || return 1
    ;;
  dnf)
    sudo dnf remove -y __PROJECT_NAME__ || return 1
    ;;
  yum)
    sudo yum remove -y __PROJECT_NAME__ || return 1
    ;;
  rpm)
    sudo rpm -e __PROJECT_NAME__ || return 1
    ;;
  apt)
    sudo apt-get remove -y __PROJECT_NAME__ || return 1
    ;;
  zypper)
    sudo zypper remove -y __PROJECT_NAME__ || return 1
    ;;
  *)
    err "Don't know how to uninstall via: ${pkm}"
    return 1
    ;;
  esac

  log "Package-manager version removed"
}

check_repo_configured() {
  local pkm="$1"

  case "${pkm}" in
  homebrew)
    if brew tap 2>/dev/null | grep -q "xooooooooox/radp"; then
      return 0
    fi
    return 1
    ;;
  dnf | yum)
    if [[ -f /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:xooooooooox:radp.repo ]] ||
      [[ -f /etc/yum.repos.d/radp.repo ]]; then
      return 0
    fi
    return 1
    ;;
  apt)
    if [[ -f /etc/apt/sources.list.d/home:xooooooooox:radp.list ]]; then
      return 0
    fi
    return 1
    ;;
  zypper)
    if zypper repos 2>/dev/null | grep -q "xooooooooox"; then
      return 0
    fi
    return 1
    ;;
  esac

  return 1
}

setup_repo() {
  local pkm="$1"

  log "Setting up repository for ${pkm}..."

  case "${pkm}" in
  homebrew)
    log "Adding Homebrew tap..."
    brew tap xooooooooox/radp
    ;;
  dnf)
    log "Enabling COPR repository..."
    sudo dnf install -y dnf-plugins-core >/dev/null 2>&1 || true
    sudo dnf copr enable -y xooooooooox/radp
    ;;
  yum)
    log "Enabling COPR repository..."
    sudo yum install -y yum-plugin-copr >/dev/null 2>&1 || true
    sudo yum copr enable -y xooooooooox/radp
    ;;
  apt)
    log "Adding OBS repository..."
    local distro=""
    if [[ -f /etc/os-release ]]; then
      # shellcheck disable=SC1091
      source /etc/os-release
      case "${ID:-}" in
      ubuntu)
        distro="xUbuntu_${VERSION_ID}"
        ;;
      debian)
        distro="Debian_${VERSION_ID}"
        ;;
      *)
        err "Unsupported distribution for apt: ${ID:-unknown}"
        return 1
        ;;
      esac
    fi
    if [[ -z "${distro}" ]]; then
      err "Cannot detect distribution for OBS repository"
      return 1
    fi
    echo "deb http://download.opensuse.org/repositories/home:/xooooooooox:/radp/${distro}/ /" |
      sudo tee /etc/apt/sources.list.d/home:xooooooooox:radp.list >/dev/null
    curl -fsSL "https://download.opensuse.org/repositories/home:xooooooooox:radp/${distro}/Release.key" |
      gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/home_xooooooooox_radp.gpg >/dev/null
    sudo apt-get update >/dev/null
    ;;
  zypper)
    log "Adding OBS repository..."
    local distro=""
    if [[ -f /etc/os-release ]]; then
      # shellcheck disable=SC1091
      source /etc/os-release
      case "${ID:-}" in
      opensuse-tumbleweed)
        distro="openSUSE_Tumbleweed"
        ;;
      opensuse-leap)
        distro="openSUSE_Leap_${VERSION_ID}"
        ;;
      sles)
        distro="SLE_${VERSION_ID}"
        ;;
      *)
        err "Unsupported distribution for zypper: ${ID:-unknown}"
        return 1
        ;;
      esac
    fi
    if [[ -z "${distro}" ]]; then
      err "Cannot detect distribution for OBS repository"
      return 1
    fi
    sudo zypper addrepo -f "https://download.opensuse.org/repositories/home:/xooooooooox:/radp/${distro}/home:xooooooooox:radp.repo"
    ;;
  *)
    err "Unknown package manager: ${pkm}"
    return 1
    ;;
  esac
}

refresh_cache() {
  local pkm="$1"

  log "Refreshing package cache..."

  case "${pkm}" in
  homebrew)
    brew update >/dev/null 2>&1 || true
    ;;
  dnf)
    sudo dnf clean all >/dev/null 2>&1 || true
    sudo dnf makecache >/dev/null 2>&1 || true
    ;;
  yum)
    sudo yum clean all >/dev/null 2>&1 || true
    sudo yum makecache >/dev/null 2>&1 || true
    ;;
  apt)
    sudo apt-get update >/dev/null 2>&1 || true
    ;;
  zypper)
    sudo zypper refresh >/dev/null 2>&1 || true
    ;;
  esac
}

# Install radp-bash-framework dependency by delegating to its install.sh
install_radp_bf() {
  local mode="$1"

  if have radp-bf; then
    log "radp-bash-framework is already installed"
    return 0
  fi

  log "Installing radp-bash-framework dependency..."

  local install_url="https://raw.githubusercontent.com/xooooooooox/radp-bash-framework/main/install.sh"
  local bin_dir="${OPT_BIN_DIR:-$HOME/.local/bin}"

  if have curl; then
    curl -fsSL "${install_url}" | bash -s -- --mode "${mode}" --bin-dir "${bin_dir}"
  elif have wget; then
    wget -qO- "${install_url}" | bash -s -- --mode "${mode}" --bin-dir "${bin_dir}"
  else
    die "curl or wget required to install radp-bash-framework"
  fi
}

install_via_pkm() {
  local pkm="$1"

  # Refresh cache to ensure we get the latest version
  refresh_cache "${pkm}"

  # Install radp-bash-framework first
  install_radp_bf "${pkm}" || die "Failed to install radp-bash-framework"

  log "Installing ${REPO_NAME} via ${pkm}..."

  case "${pkm}" in
  homebrew)
    brew install __PROJECT_NAME__
    ;;
  dnf)
    sudo dnf install -y __PROJECT_NAME__
    ;;
  yum)
    sudo yum install -y __PROJECT_NAME__
    ;;
  apt)
    sudo apt-get install -y __PROJECT_NAME__
    ;;
  zypper)
    sudo zypper install -y __PROJECT_NAME__
    ;;
  *)
    err "Unknown package manager: ${pkm}"
    return 1
    ;;
  esac
}

# ============================================================================
# Manual Installation
# ============================================================================

detect_fetcher() {
  if have curl; then
    echo "curl"
    return 0
  fi
  if have wget; then
    echo "wget"
    return 0
  fi
  if have fetch; then
    echo "fetch"
    return 0
  fi
  return 1
}

fetch_url() {
  local tool="$1"
  local url="$2"
  local out="$3"

  case "${tool}" in
  curl)
    curl -fsSL "${url}" -o "${out}"
    ;;
  wget)
    wget -qO "${out}" "${url}"
    ;;
  fetch)
    fetch -qo "${out}" "${url}"
    ;;
  *)
    return 1
    ;;
  esac
}

fetch_text() {
  local tool="$1"
  local url="$2"

  case "${tool}" in
  curl)
    curl -fsSL "${url}"
    ;;
  wget)
    wget -qO- "${url}"
    ;;
  fetch)
    fetch -qo- "${url}"
    ;;
  *)
    return 1
    ;;
  esac
}

resolve_ref() {
  if [[ -n "${OPT_REF}" ]]; then
    echo "${OPT_REF}"
    return 0
  fi

  if [[ -n "${OPT_VERSION}" ]]; then
    echo "${OPT_VERSION}"
    return 0
  fi

  local api_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest"
  local json
  json="$(fetch_text "${FETCH_TOOL}" "${api_url}" || true)"
  if [[ -z "${json}" ]]; then
    die "Failed to fetch latest release; use --ref <branch|tag> to specify."
  fi

  local tag
  tag="$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<<"${json}")"
  tag="${tag%%$'\n'*}"
  if [[ -z "${tag}" ]]; then
    die "Failed to parse latest tag; use --ref <branch|tag> to specify."
  fi
  echo "${tag}"
}

cleanup() {
  if [[ -n "${tmp_dir:-}" ]]; then
    rm -rf "${tmp_dir}"
  fi
}

install_manual() {
  FETCH_TOOL="$(detect_fetcher)" || die "Requires curl, wget, or fetch."

  local install_dir="${OPT_INSTALL_DIR:-$HOME/.local/lib/${REPO_NAME}}"
  local bin_dir="${OPT_BIN_DIR:-$HOME/.local/bin}"
  local ref
  ref="$(resolve_ref)"

  # Install radp-bash-framework first
  install_radp_bf "manual" || die "Failed to install radp-bash-framework"

  if [[ -z "${install_dir}" || "${install_dir}" == "/" ]]; then
    die "Unsafe install dir: ${install_dir}"
  fi
  if [[ "${__PROJECT_UPPER___ALLOW_ANY_DIR:-0}" != "1" ]]; then
    if [[ "$(basename "${install_dir}")" != "${REPO_NAME}" ]]; then
      die "Install dir must end with ${REPO_NAME} (set __PROJECT_UPPER___ALLOW_ANY_DIR=1 to override)."
    fi
  fi

  local tar_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/${ref}.tar.gz"
  tmp_dir="$(mktemp -d 2>/dev/null || mktemp -d -t "${REPO_NAME}")"
  local tarball="${tmp_dir}/${REPO_NAME}.tar.gz"
  trap cleanup EXIT

  log "Downloading ${tar_url}"
  if ! fetch_url "${FETCH_TOOL}" "${tar_url}" "${tarball}"; then
    die "Failed to download ${tar_url}"
  fi

  local tar_listing
  tar_listing="$(tar -tzf "${tarball}")"
  local root_dir="${tar_listing%%/*}"
  if [[ -z "${root_dir}" ]]; then
    die "Unable to read archive structure."
  fi

  tar -xzf "${tarball}" -C "${tmp_dir}"
  local src_root="${tmp_dir}/${root_dir}"
  if [[ ! -d "${src_root}/bin" || ! -d "${src_root}/src" ]]; then
    die "Archive layout unexpected; missing bin/ or src/."
  fi

  rm -rf "${install_dir}"
  mkdir -p "${install_dir}"
  cp -R "${src_root}/bin" "${install_dir}/"
  cp -R "${src_root}/src" "${install_dir}/"

  # Remove IDE support files (development only, not needed at runtime)
  find "${install_dir}/src" -name "_ide*.sh" -delete 2>/dev/null || true

  chmod 0755 "${install_dir}/bin/__PROJECT_NAME__"
  find "${install_dir}/src" -type f -name "*.sh" -exec chmod 0755 {} \;

  # Write install metadata
  echo "${REPO_OWNER}/${REPO_NAME}" >"${install_dir}/.install-repo"
  echo "manual" >"${install_dir}/.install-method"
  echo "${ref}" >"${install_dir}/.install-ref"

  # Write actual installed version for banner display
  local installed_version
  if [[ "${ref}" =~ ^v[0-9]+\.[0-9]+ ]]; then
    installed_version="${ref}"
  else
    local base_version
    base_version=$(grep -oE 'version:[[:space:]]*v[0-9]+\.[0-9]+\.[0-9]+' \
      "${install_dir}/src/main/shell/config/config.yaml" 2>/dev/null \
      | head -1 | sed 's/.*version:[[:space:]]*//' || echo "v0.0.0")
    installed_version="${base_version}+${ref}"
  fi
  echo "${installed_version}" >"${install_dir}/.install-version"

  mkdir -p "${bin_dir}"
  local target="${install_dir}/bin/__PROJECT_NAME__"
  local link_path="${bin_dir}/__PROJECT_NAME__"
  if [[ -e "${link_path}" && ! -L "${link_path}" ]]; then
    die "Refusing to overwrite existing file: ${link_path}"
  fi
  ln -sf "${target}" "${link_path}"

  log ""
  log "Installed ${REPO_NAME} (ref: ${ref}) to ${install_dir}"
  log "Symlinked to ${link_path}"
  log "Ensure ${bin_dir} is in your PATH."
}

setup_completion() {
  local bin_dir="${OPT_BIN_DIR:-$HOME/.local/bin}"
  local install_dir="${OPT_INSTALL_DIR:-$HOME/.local/lib/${REPO_NAME}}"
  local cmd=""

  # Find command - prefer system path, then bin_dir, then install_dir
  if have __PROJECT_NAME__; then
    cmd="__PROJECT_NAME__"
  elif [[ -x "${bin_dir}/__PROJECT_NAME__" ]]; then
    cmd="${bin_dir}/__PROJECT_NAME__"
  elif [[ -x "${install_dir}/bin/__PROJECT_NAME__" ]]; then
    cmd="${install_dir}/bin/__PROJECT_NAME__"
  fi

  # For package manager installs, try system paths
  if [[ -z "${cmd}" ]]; then
    for path in /usr/bin/__PROJECT_NAME__ /usr/local/bin/__PROJECT_NAME__ /opt/homebrew/bin/__PROJECT_NAME__; do
      if [[ -x "${path}" ]]; then
        cmd="${path}"
        break
      fi
    done
  fi

  if [[ -z "${cmd}" ]]; then
    log "Warning: __PROJECT_NAME__ not found, skipping completion setup"
    return 0
  fi

  # Detect user's shell
  local user_shell
  user_shell="$(basename "${SHELL:-/bin/bash}")"

  case "${user_shell}" in
  bash)
    setup_bash_completion "${cmd}"
    ;;
  zsh)
    setup_zsh_completion "${cmd}"
    ;;
  *)
    log "Unknown shell: ${user_shell}, skipping completion setup"
    log "Run '__PROJECT_NAME__ completion bash' or '__PROJECT_NAME__ completion zsh' manually"
    ;;
  esac
}

setup_bash_completion() {
  local cmd="$1"
  local comp_dir="$HOME/.local/share/bash-completion/completions"
  local comp_file="${comp_dir}/__PROJECT_NAME__"

  log "Setting up bash completion..."

  # Create completions directory
  mkdir -p "${comp_dir}"

  # Generate completion script
  if "${cmd}" completion bash >"${comp_file}" 2>/dev/null; then
    log "Bash completion installed to ${comp_file}"
  else
    log "Warning: Failed to generate bash completion"
    return 0
  fi

  # Check if bash-completion is installed
  if ! have bash; then
    return 0
  fi

  # Hint about bash-completion package
  local bash_completion_loaded=false
  if [[ -f /usr/share/bash-completion/bash_completion ]] ||
    [[ -f /etc/bash_completion ]] ||
    [[ -f /opt/homebrew/etc/profile.d/bash_completion.sh ]] ||
    [[ -f /usr/local/etc/profile.d/bash_completion.sh ]]; then
    bash_completion_loaded=true
  fi

  if [[ "${bash_completion_loaded}" != "true" ]]; then
    log ""
    log "Note: bash-completion package may not be installed."
    log "Install it for completion to work:"
    local os
    os="$(detect_os)"
    case "${os}" in
    macos)
      log "  brew install bash-completion@2"
      ;;
    rhel)
      log "  sudo dnf install bash-completion"
      ;;
    debian)
      log "  sudo apt install bash-completion"
      ;;
    esac
  fi
}

setup_zsh_completion() {
  local cmd="$1"
  local comp_dir="$HOME/.zfunc"
  local comp_file="${comp_dir}/___PROJECT_NAME__"

  log "Setting up zsh completion..."

  # Create completions directory
  mkdir -p "${comp_dir}"

  # Generate completion script
  if "${cmd}" completion zsh >"${comp_file}" 2>/dev/null; then
    log "Zsh completion installed to ${comp_file}"
  else
    log "Warning: Failed to generate zsh completion"
    return 0
  fi

  # Check if fpath includes the directory
  log ""
  log "Ensure ~/.zfunc is in your fpath. Add to ~/.zshrc:"
  log '  fpath=(~/.zfunc $fpath)'
  log '  autoload -Uz compinit && compinit'
}

print_post_install() {
  local bin_dir="${OPT_BIN_DIR:-$HOME/.local/bin}"

  log ""

  # Setup completion
  setup_completion

  log ""
  log "Installation complete!"

  # Check if bin_dir is in PATH
  if [[ ":$PATH:" != *":${bin_dir}:"* ]]; then
    log ""
    log "Note: ${bin_dir} is not in your PATH."
    log "Add it to your shell profile:"
    log "  export PATH=\"${bin_dir}:\$PATH\""
  fi

  log ""
  log "Run: hash -r && __PROJECT_NAME__ --help"
}

# ============================================================================
# Main
# ============================================================================

main() {
  parse_args "$@"

  local mode="${OPT_MODE}"

  # --ref implies manual installation with pkm conflict resolution
  if [[ -n "${OPT_REF}" ]]; then
    log "Installing from ref: ${OPT_REF}"

    local existing_pkm
    existing_pkm="$(detect_pkm_installed)"
    if [[ -n "${existing_pkm}" ]]; then
      log "Detected existing package-manager installation (${existing_pkm})"
      uninstall_pkm "${existing_pkm}" || die "Failed to remove package-manager version"
    fi

    install_manual
    print_post_install
    return 0
  fi

  local pkm=""

  case "${mode}" in
  manual)
    log "Using manual installation (--mode manual)"
    install_manual
    print_post_install
    return 0
    ;;
  homebrew | dnf | yum | apt | zypper)
    pkm="${mode}"
    if ! have "${pkm}" && [[ "${pkm}" != "homebrew" ]]; then
      die "Package manager '${pkm}' not found"
    fi
    if [[ "${pkm}" == "homebrew" ]] && ! have brew; then
      die "Homebrew not found"
    fi
    ;;
  auto | "")
    pkm="$(detect_package_manager)"
    if [[ -z "${pkm}" ]]; then
      log "No supported package manager detected, using manual installation"
      install_manual
      print_post_install
      return 0
    fi
    log "Detected package manager: ${pkm}"
    ;;
  *)
    die "Unknown install mode: ${mode}. Use: auto, manual, homebrew, dnf, yum, apt, zypper"
    ;;
  esac

  # Check for existing manual installation and remove it
  local manual_install_dir="${OPT_INSTALL_DIR:-$HOME/.local/lib/${REPO_NAME}}"
  if [[ -f "${manual_install_dir}/.install-method" ]] &&
    [[ "$(cat "${manual_install_dir}/.install-method")" == "manual" ]]; then
    log "Detected existing manual installation at ${manual_install_dir}"
    log "Removing manual installation to avoid conflicts..."
    local manual_bin_dir="${OPT_BIN_DIR:-$HOME/.local/bin}"
    local manual_link="${manual_bin_dir}/__PROJECT_NAME__"
    if [[ -L "${manual_link}" ]]; then
      rm -f "${manual_link}"
    fi
    rm -rf "${manual_install_dir}"
    log "Manual installation removed"
  fi

  if ! check_repo_configured "${pkm}"; then
    log "Repository not configured for ${pkm}"
    setup_repo "${pkm}" || {
      err "Failed to setup repository, falling back to manual installation"
      install_manual
      print_post_install
      return 0
    }
  fi

  install_via_pkm "${pkm}" || {
    err "Package manager installation failed, falling back to manual installation"
    install_manual
    print_post_install
    return 0
  }

  log "Successfully installed ${REPO_NAME} via ${pkm}"
  print_post_install
}

main "$@"
INSTALL_SCRIPT

  # Note: Caller must replace placeholders:
  # - __PROJECT_NAME__ -> project_name
  # - __PROJECT_UPPER__ -> PROJECT_NAME (uppercase)
  # - __PROJECT_VAR__ -> project_var
}

#######################################
# Generate Debian control file content
# Arguments:
#   1 - project_name: Project name
# Outputs:
#   Complete debian/control content to stdout
#######################################
radp_packaging_content_debian_control() {
  local project_name="$1"

  cat <<CONTROL
Source: ${project_name}
Section: utils
Priority: optional
Maintainer: xooooooooox <xozoz.sos@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/xooooooooox/${project_name}
Rules-Requires-Root: no

Package: ${project_name}
Architecture: all
Depends: \${misc:Depends}, bash, coreutils, radp-bash-framework
Description: CLI tool built with radp-bash-framework.
 ${project_name} is a CLI tool built with radp-bash-framework.
CONTROL
}

#######################################
# Generate Debian rules file content
# Arguments:
#   1 - project_name: Project name
# Outputs:
#   Complete debian/rules content to stdout
#######################################
radp_packaging_content_debian_rules() {
  local project_name="$1"

  cat <<RULES
#!/usr/bin/make -f

%:
	dh \$@

override_dh_auto_configure:
	:

override_dh_auto_build:
	:

override_dh_auto_install:
	:

override_dh_fixperms:
	dh_fixperms
	# Remove IDE support files (development only, not needed at runtime)
	find debian/${project_name}/usr/lib/${project_name}/src -name "_ide*.sh" -delete 2>/dev/null || true
	chmod 0755 debian/${project_name}/usr/lib/${project_name}/bin/${project_name}
	find debian/${project_name}/usr/lib/${project_name}/src -type f -name '*.sh' -exec chmod 0755 {} \;
RULES
}

#######################################
# Generate Debian install file content
# Arguments:
#   1 - project_name: Project name
# Outputs:
#   Complete debian/<project>.install content to stdout
#######################################
radp_packaging_content_debian_install() {
  local project_name="$1"

  cat <<INSTALL
bin usr/lib/${project_name}
src usr/lib/${project_name}
completions/${project_name}.bash usr/share/bash-completion/completions/${project_name}
completions/${project_name}.zsh usr/share/zsh/vendor-completions/_${project_name}
INSTALL
}

#######################################
# Generate Debian links file content
# Arguments:
#   1 - project_name: Project name
# Outputs:
#   Complete debian/<project>.links content to stdout
#######################################
radp_packaging_content_debian_links() {
  local project_name="$1"

  cat <<LINKS
usr/lib/${project_name}/bin/${project_name} usr/bin/${project_name}
LINKS
}

#######################################
# Generate Debian changelog file content
# Arguments:
#   1 - project_name: Project name
# Outputs:
#   Complete debian/changelog content to stdout
#######################################
radp_packaging_content_debian_changelog() {
  local project_name="$1"

  cat <<CHANGELOG
${project_name} (0.0.0-1) unstable; urgency=medium

  * Placeholder entry. The CI workflow rewrites this changelog.

 -- xooooooooox <xozoz.sos@gmail.com>  Thu, 01 Jan 1970 00:00:00 +0000
CHANGELOG
}

#######################################
# Generate Debian copyright file content
# Arguments:
#   1 - project_name: Project name
# Outputs:
#   Complete debian/copyright content to stdout
#######################################
radp_packaging_content_debian_copyright() {
  local project_name="$1"

  cat <<COPYRIGHT
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ${project_name}
Source: https://github.com/xooooooooox/${project_name}

Files: *
Copyright: 2024-present xooooooooox
License: MIT

License: MIT
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
 .
 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.
 .
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
COPYRIGHT
}
