#!/usr/bin/env bash

# Copyright 2025-2026, Paul Johnson (paul@pjcj.net)

# This software is free.  It is licensed under the same terms as Perl itself.

# The latest version of this software should be available from my homepage:
# https://pjcj.net

if ((BASH_VERSINFO[0] < 5)); then
  echo "bash version $BASH_VERSION is too old. Please install v5 or higher."
  exit 1
fi

set -eEuo pipefail
shopt -s inherit_errexit

_p() {
  __l="$(hostname): $1"
  shift
  echo "$__l $script: $*" | tee -a "$LOG_FILE" >&2
}
pi() { _p "[INFO]   " "$*"; }
pe() { _p "[ERROR]  " "$*"; }
pf() {
  _p "[FATAL]  " "$*"
  exit 1
}

usage() {
  cat <<EOT
$script --help
$script --verbose --dryrun release-ticket
$script --dryrun release
$script release
EOT
  exit 0
}

cleanup() {
  declare -r res=$?
  exit "$res"
}

parse_options() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
    -d | --dryrun)
      dryrun=1
      shift
      ;;
    -h | --help)
      usage
      ;;
    -t | --trace)
      set -x
      shift
      ;;
    -v | --verbose)
      verbose=1
      shift
      ;;
    *)
      recipe="$1"
      shift
      args=("$@")
      break
      ;;
    esac
  done
}

recipe_options() {
  echo "-d --dryrun"
  echo "-h --help"
  echo "-t --trace"
  echo "-v --verbose"
  declare -F | perl -nE 'say $1 if /recipe_(.+)/'
}

setup() {
  script=$(basename "$0")
  readl=readlink
  if command -v greadlink >&/dev/null; then readl=greadlink; fi
  srcdir=$("$readl" -f "$(dirname "$0")")
  readonly LOG_FILE="/tmp/$script.log"

  PATH="$srcdir:$PATH"

  dryrun=0
  verbose=0
  recipe=""
  args=()

  parse_options "$@"
}

setup_hooks() {
  if [[ ! -d .git/hooks ]]; then
    pi "No .git/hooks directory - skipping hook setup"
    return 0
  fi
  for hook in commit-msg prepare-commit-msg; do
    for link in ".git/hooks/$hook" ".git/hooks/$hook.legacy"; do
      if [[ -L $link && $(readlink "$link") == *"utils/${hook}-hook" ]]; then
        rm "$link"
        pi "Removed legacy $hook symlink"
      fi
    done
  done
  if command -v pre-commit >/dev/null 2>&1; then
    pre-commit install --install-hooks
  else
    pi "pre-commit not found, skipping"
    pi "  Install with: brew install pre-commit"
    pi "  Then run: pre-commit install --install-hooks"
  fi
}

recipe_setup() {
  setup_hooks
}

confirm() {
  local description="$1"
  if ((dryrun)); then
    pi "[dry run] would: $description"
    return 0
  fi
  echo ""
  pi "About to: $description"
  local answer
  read -rp "Continue? [yes/NO] " answer
  if [[ ${answer,,} != "yes" && ${answer,,} != "y" ]]; then
    pf "Aborted"
  fi
}

detect_version() {
  last_version=$(
    perl -nE 'if (/^## (v\S+)/) { say $1; exit }' Changes.md
  )
  if [[ -z $last_version ]]; then
    pf "Could not find a released version in Changes.md"
  fi
  if ((verbose)); then pi "Last released version: $last_version"; fi

  detected_version=$(
    perl -nE 'if (/^version\s*=\s*(\S+)/) { say $1; exit }' dist.ini
  )
  if [[ -z $detected_version ]]; then
    pf "Could not find version in dist.ini"
  fi
  if ((verbose)); then pi "Current version in dist.ini: $detected_version"; fi
}

prompt_version_bump() {
  if [[ $detected_version != "$last_version" ]]; then
    pi "Version already bumped to $detected_version"
    return
  fi

  pi "Current version ($detected_version) matches last release"

  local major minor patch
  local ver="${detected_version#v}"
  IFS='.' read -r major minor patch <<<"$ver"

  local bump_patch="v$major.$minor.$((patch + 1))"
  local bump_minor="v$major.$((minor + 1)).0"
  local bump_major="v$((major + 1)).0.0"

  echo ""
  echo "Choose new version:"
  echo "  1) patch  $bump_patch"
  echo "  2) minor  $bump_minor"
  echo "  3) major  $bump_major"
  echo "  4) custom"
  echo ""

  local choice
  read -rp "Choice [1]: " choice
  choice="${choice:-1}"

  case "$choice" in
  1) detected_version="$bump_patch" ;;
  2) detected_version="$bump_minor" ;;
  3) detected_version="$bump_major" ;;
  4)
    read -rp "Enter version (e.g. v1.2.3): " detected_version
    if [[ ! $detected_version =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
      pf "Invalid version: '$detected_version' (expected v1.2.3)"
    fi
    ;;
  *) pf "Invalid choice: $choice" ;;
  esac
}

release_ticket_body() {
  local version="$1"
  cat <<TICKET
## Release $version

### Automated steps (\`make release\`)

1. Create this ticket and branch
2. Commit version bump to \`dist.ini\`
3. Push branch and create PR
4. Merge PR to \`main\`
5. \`dzil release\` (CPAN upload, tag, push)

### Pre-release checklist

- [ ] \`Changes.md\` has entries under \`{{\$NEXT}}\`
- [ ] Tests pass (\`make test\`)
- [ ] Lint passes (\`make lint\`)

### Release command

\`\`\`bash
make release
\`\`\`
TICKET
}

release_ticket() {
  # Must be on main branch
  local current_branch
  current_branch=$(git branch --show-current)
  if [[ $current_branch != "main" ]]; then
    pf "Must be on main branch (currently on $current_branch)"
  fi
  pi "On main branch"

  # Working tree must be clean (Changes.md excluded - it gets
  # committed on the release branch)
  if [[ -n $(git status --porcelain -- ':!Changes.md') ]]; then
    pf "Working tree is not clean - commit or stash changes first"
  fi
  pi "Working tree is clean (excluding Changes.md)"

  # Step 1: Determine version
  local last_version detected_version
  detect_version
  prompt_version_bump

  # Step 2: Bump dist.ini before any remote artefacts exist
  if [[ $detected_version != "$last_version" ]]; then
    if ((dryrun)); then
      pi "[dry run] would update dist.ini to $detected_version"
    else
      pi "Updating dist.ini to $detected_version"
      perl -pi -e \
        "s/^version\\s*=\\s*\\S+/version = $detected_version/" dist.ini
    fi
  fi

  # Step 3: Create GitHub ticket
  local title="Release $detected_version"
  local body
  body=$(release_ticket_body "$detected_version")

  confirm "create GitHub ticket: $title"
  local issue_url issue_num
  if ((dryrun)); then
    issue_num="NNN"
  else
    issue_url=$(gh issue create --title "$title" --body "$body")
    pi "Created release ticket: $issue_url"
    issue_num="${issue_url##*/}"
  fi

  # Step 4: Create release branch
  local release_branch="GH-${issue_num}-release"
  confirm "create branch $release_branch from main"
  if ((!dryrun)); then
    git checkout -b "$release_branch" main
  fi

  # Step 5: Commit changelog and version bump (local, no checkpoint)
  if ((dryrun)); then
    pi "[dry run] would commit Changes.md and dist.ini"
  else
    git add Changes.md dist.ini
    git commit -m "Release $detected_version"
  fi

  # Step 6: Push branch and create PR
  confirm "push branch and create PR"
  local pr_body="${body}"$'\n\n'"Closes #${issue_num}"
  if ((dryrun)); then
    release_pr_url="https://github.com/.../pull/NNN"
    pi "[dry run] would push $release_branch and create PR"
  else
    git push -u origin "$release_branch"
    release_pr_url=$(
      gh pr create \
        --title "$title" \
        --body "$pr_body" \
        --base main
    )
    pi "Created PR: $release_pr_url"
  fi
}

recipe_release-ticket() {
  release_ticket
  echo ""
  pi "Remaining steps: review PR, merge, then run 'utils/run release'"
}

release() {
  # Changes.md must have content under {{$NEXT}}
  local next_content
  next_content=$(
    perl -0777 -nE '
      if (/\{\{\$NEXT\}\}\n\n(.+?)(?=\n##|\z)/s) { say $1; exit }
    ' Changes.md
  )
  if [[ -z $next_content ]]; then
    # shellcheck disable=SC2016
    pf 'Changes.md has no entries under {{$NEXT}}'
  fi
  # shellcheck disable=SC2016
  pi 'Changes.md has entries under {{$NEXT}}'

  # Steps 1-5: create ticket, branch, push, PR
  release_ticket

  # Step 6: Pause for PR review
  echo ""
  pi "PR ready for review: $release_pr_url"
  pi "Review the PR, then continue to merge and release."
  if ((dryrun)); then
    pi "[dry run] skipping PR review pause"
  else
    local answer
    read -rp "Continue after PR review? [yes/NO] " answer
    if [[ ${answer,,} != "yes" && ${answer,,} != "y" ]]; then
      pf "Aborted"
    fi
  fi

  # Step 7: Merge PR
  confirm "merge PR"
  if ((!dryrun)); then
    gh pr merge --merge "$release_pr_url"
  fi

  # Step 8: Check out main and pull (local, no checkpoint)
  if ((dryrun)); then
    pi "[dry run] would check out main and pull"
  else
    git checkout main
    git pull
  fi

  # Step 9: dzil release (ConfirmRelease is the checkpoint)
  if ((dryrun)); then
    pi "[dry run] would run: dzil release"
  else
    pi "Running dzil release"
    dzil release
  fi

  # Step 10: Post-release verification
  pi "Post-release verification:"
  pi "  - Check https://metacpan.org/dist/Perl-Critic-PJCJ for new version"
  pi "  - Verify git tag: git tag -l"
  # shellcheck disable=SC2016
  pi '  - Confirm Changes.md has version heading and fresh {{$NEXT}}'
}

recipe_release() {
  release
}

run_recipe() {
  local fn="recipe_$recipe"
  if declare -F "$fn" >/dev/null 2>&1; then
    "$fn" "${args[@]:-}"
  else
    pf "Unknown recipe: $recipe"
  fi
}

main() {
  setup "$@"
  ((verbose)) && pi "Running $recipe ${args[*]:-}"
  [[ -z ${recipe:-} ]] && pf "Missing recipe"
  run_recipe "${args[@]:-}"
}

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
  trap cleanup EXIT INT
  main "$@"
fi
