#!/usr/bin/env bash

# Copyright 2001-2025, 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
}
pt() { _p "[TRACE]  " "$*"; }
pd() { _p "[DEBUG]  " "$*"; }
pi() { _p "[INFO]   " "$*"; }
pw() { _p "[WARNING]" "$*"; }
pe() { _p "[ERROR]  " "$*"; }
pf() {
  _p "[FATAL]  " "$*"
  exit 1
}

usage() {
  cat <<EOT
$script --help
$script --trace --verbose
$script --env=dev cpancover-controller-run-once
$script --results_dir=/cover/dev --image=pjcj/cpancover_dev cpancover-run
EOT
  exit 0
}

cleanup() {
  declare -r res=$?
  # ((verbose)) && pi "Cleaning up"
  exit "$res"
}

parse_options() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
    -d | --dryrun)
      dryrun=1
      shift
      ;;
    -e | --env)
      shift
      env="$1"
      case "$env" in
      prod)
        results_dir=~/cover/staging
        docker_image=pjcj/cpancover
        ;;
      dev)
        results_dir=~/cover/staging_dev
        docker_image=pjcj/cpancover_dev
        ;;
      *)
        pf "Unrecognised environment: $1"
        ;;
      esac
      shift
      if grep -q "/docker/" /proc/self/mountinfo 2>/dev/null; then
        # we're inside a docker container so use the remote staging directory
        results_dir=/remote_staging
      fi
      ;;
    -f | --force)
      force=1
      shift
      ;;
    -h | --help)
      usage
      ;;
    -i | --image)
      docker_image="$2"
      shift 2
      ;;
    -r | --results_dir)
      results_dir="$2"
      shift 2
      ;;
    -t | --trace)
      set -x
      shift
      ;;
    -v | --verbose)
      verbose=1
      shift
      ;;
    *)
      recipe="$1"
      shift
      args=("$@")
      break
      ;;
    esac
  done
}

recipe_options() {
  echo "-d --dryrun"
  echo "-e --env"
  echo "-f --force"
  echo "-h --help"
  echo "-i --image"
  echo "-r --results_dir"
  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"

  export AUTOMATED_TESTING=1
  export NONINTERACTIVE_TESTING=1
  export EXTENDED_TESTING=1

  PATH="$srcdir:$PATH"

  docker=docker
  docker_image=pjcj/cpancover
  dryrun=0
  env=prod
  force=0
  results_dir=~/cover/staging
  verbose=0

  parse_options "$@"
}

nice_cpus() {
  perl -Iutils -MDevel::Cover::BuildUtils=nice_cpus -e "print nice_cpus"
}

recipe_nice-cpus() {
  nice_cpus
}

recipe_update-copyright() {
  local from="${1:-$(date +'%Y' --date='last year')}"
  local to="${2:-$(date +'%Y')}"
  pi "Updating copyright from $from to $to"
  local me="Paul Johnson"
  local files
  files=$(git ls-files)
  # shellcheck disable=SC2086
  perl -pi -e "s/Copyright \\d+-\\K$from(, $me)/$to\$1/i" $files
  # shellcheck disable=SC2086
  perl -pi -e "s/Copyright $from\\K(, $me)/-$to\$1/i" $files
}

get_cpm() {
  cpanm --notest App::cpm
  plenv rehash
  cpm=$(plenv which cpm)
}

install_dependencies() {
  get_cpm
  pi "Installing dependencies with $cpm"
  $cpm install --workers="$(nice_cpus)" --global \
    Sereal Digest::MD5 Template Pod::Coverage::CountParents \
    Capture::Tiny Parallel::Iterator Template Class::XSAccessor
}

install_development_dependencies() {
  get_cpm
  $cpm install --workers="$(nice_cpus)" --global \
    Dist::Zilla Perl::Critic Perl::Tidy App::perlimports \
    Perl::Critic::PJCJ
  plenv rehash
  dzil authordeps --missing |
    xargs "$cpm" install --workers="$(nice_cpus)" --global
  dzil listdeps --missing |
    xargs "$cpm" install --workers="$(nice_cpus)" --global
}

install_test_dependencies() {
  get_cpm
  $cpm install --workers="$(nice_cpus)" --global \
    DBM::Deep
}

recipe_install-dependencies() {
  install_dependencies
}

recipe_install-development-dependencies() {
  install_development_dependencies
}

recipe_install-test-dependencies() {
  install_test_dependencies
}

install_perl() {
  local name="${1:?No name specified}"
  local version="${2:?No version specified}"
  yes | plenv uninstall "$name" || true
  plenv install --as "$name" -j 32 -D usedevel --noman "$version"
  export PLENV_VERSION="$name"
  plenv install-cpanm
  install_dependencies
}

recipe_install-perl() {
  local name="${1:?No name specified}"
  local version="${2:?No version specified}"
  install_perl "$name" "$version"
}

recipe_install-cpancover-perl() {
  local version="${1:?No version specified}"
  install_perl cpancover "$version"
}

recipe_install-dc-dev-perl() {
  local version="${1:?No version specified}"
  install_perl dc-dev "$version"
  install_development_dependencies
}

recipe_all-versions() {
  ./utils/all_versions "$@"
}

run_cpancover() {
  mkdir -p "$results_dir"
  local cpancover=cpancover
  if [[ $(pwd) != /dc ]]; then
    local root=
    [[ -d /dc ]] && root=/dc/
    PATH="./utils:./bin:$PATH"
    perl Makefile.PL && make
    cpancover="perl -Mblib=$root ${root}bin/cpancover --local"
  fi
  ((verbose)) && cpancover="$cpancover --verbose"
  ((force)) && cpancover="$cpancover --force"
  ((dryrun)) && cpancover="$cpancover --dryrun"
  local cmd
  cmd="$cpancover --env $env --results_dir $results_dir"
  cmd="$cmd --workers $(nice_cpus) $*"
  ((verbose)) && pi "$cmd"
  $cmd || true
}

recipe_cpancover() {
  run_cpancover "${args[@]:-}"
}

cpancover_docker_ps() {
  local name="${docker_image//[^a-zA-Z0-9_.]/-}"
  $docker ps -a | tail -n +2 | grep "$name-" | grep -vw "$(hostname)"
}

recipe_cpancover-docker-ps() {
  cpancover_docker_ps
}

cpancover_docker_ps_ids() {
  cpancover_docker_ps | awk '{ print $1 }' || true
}

recipe_cpancover-docker-kill() {
  cpancover_docker_ps_ids | xargs -r "$docker" kill
}

cpancover_docker_rm() {
  cpancover_docker_ps_ids | xargs -r "$docker" rm -f
  $docker system prune --force
}

recipe_cpancover-docker-rm() {
  cpancover_docker_rm
}

recipe_cpancover-docker-rm-image() {
  $docker ps -q --filter ancestor="$docker_image" | xargs -r "$docker" stop
  $docker ps -aq --filter ancestor="$docker_image" | xargs -r "$docker" rm
  $docker rmi "$docker_image"
}

cpancover_latest() {
  run_cpancover --latest
}

recipe_cpancover-latest() {
  cpancover_latest
}

recipe_cpancover-build-module() {
  local module="${1:?No module specified}"
  local v=
  ((verbose)) && v=--verbose
  run_cpancover "$v" --local_build --docker "$docker" --workers 1 "$module"
}

docker_name() {
  local name="${1:?No name specified}"
  name="$docker_image-$name-$(date +%s+%N)"
  echo "${name//[^a-zA-Z0-9_.]/-}"
}

recipe_docker-name() {
  docker_name "${args[@]:-}"
}

cpancover_controller_command() {
  local name="${1:?No name specified}"
  shift
  local cmd=("$@")
  mkdir -p "$results_dir"
  local sock=/var/run/docker.sock
  $docker run -it \
    --mount type=bind,source=$sock,target=$sock \
    --mount type=bind,source="$results_dir",target=/remote_staging \
    --rm=false --memory=1g --name="$(docker_name "$name")" \
    "$docker_image" "${cmd[@]}"
}

recipe_cpancover-controller-shell() {
  cpancover_controller_command controller_bash "/bin/bash"
}

recipe_cpancover-docker-shell() {
  local staging="${1:-$results_dir}"
  mkdir -p "$staging"
  $docker run -it \
    --mount type=bind,source="$staging",target=/remote_staging \
    --rm=false --memory=1g --name="$(docker_name docker)" \
    "$docker_image" /bin/bash
}

# Called from Collection.pm
recipe_cpancover-docker-module() {
  local module="${1:?No module specified}"
  local name="${2:?No name specified}"
  local staging="${3:-$results_dir}"

  name=$(docker_name "$name")
  mkdir -p "$staging"
  ((verbose)) && pi "module: $module"
  local v=
  ((verbose)) && v=--verbose
  container=$($docker run -d \
    --rm=false --memory=1g --name="$name" \
    "$docker_image" \
    dc $v cpancover-build-module "$module")
  ((verbose)) && pi "container is $container"
  $docker wait "$name" # >/dev/null
  # shellcheck disable=SC2181
  if [[ $? == 0 ]]; then
    $docker logs "$name"
    local_staging="$staging/$name"
    mkdir -p "$local_staging"
    $docker cp "$name:/root/cover/staging" "$local_staging"
    if [[ -d $local_staging ]]; then
      chmod -R 755 "$local_staging"
      find "$local_staging" -type f -exec chmod 644 {} \;
      chown -R "$(id -un):$(id -gn)" "$local_staging"
      cd "$local_staging"/* || exit
      for f in *; do
        if [[ -d $f ]]; then
          rm -rf "${staging:?}/$f"
          mv "$f" "$staging"
        fi
      done
      rm -r "$local_staging"
    fi
  fi
  $docker rm "$name" >/dev/null
}

cpancover_compress() {
  rm -rf "$results_dir"/*/{runs,structure}
  find "$results_dir/" -name __failed__ -prune -o \
    -type f -not -name '*.gz' -not -name '*.json' \
    -exec gzip -f9 {} \;
}

recipe_cpancover-compress() {
  cpancover_compress
}

recipe_cpancover-uncompress-dir() {
  subdir="${1:?No subdir specified}"
  find "$results_dir/$subdir/" -name __failed__ -prune -o \
    -type f -name '*.gz' \
    -exec gzip -d {} \;
}

cpancover_compress_old_versions() {
  keep="${1:-3}"
  run_cpancover --nobuild --compress_old_versions "$keep"
}

recipe_cpancover-compress-old-versions() {
  cpancover_compress_old_versions "${args[@]:-}"
}

cpancover_generate_html() {
  pi "Generating HTML at $(date)"
  cpancover_compress_old_versions
  run_cpancover --generate_html
  cpancover_compress
  local json=$results_dir/cpancover.json
  local tmp=$json-tmp-$$.gz
  pi "Compressing $json"
  pigz <"$json" >"$tmp" && mv "$tmp" "$json.gz"
  pi "Done"
}

recipe_cpancover-generate-html() {
  cpancover_generate_html
}

cpancover_run_once() {
  pi "Starting cpancover run at $(date) on $(nice_cpus) cpus"
  cpancover_docker_rm
  cpancover_latest | run_cpancover --build
  cpancover_generate_html
  pi "Finished cpancover run at $(date)"
}

cpancover_run_loop() {
  while true; do
    cpancover_run_once
    sleep 600 # 10 minutes
  done
}

recipe_cpancover-run-once() {
  cpancover_run_once
}

recipe_cpancover-run-loop() {
  cpancover_run_loop
}

recipe_cpancover-controller-run() {
  local o=(--env "$env")
  ((verbose)) && o+=("--verbose")
  cpancover_controller_command controller dc "${o[@]}" cpancover-run-loop
}

recipe_cpancover-controller-run-once() {
  local o=(--env "$env")
  ((verbose)) && o+=("--verbose")
  cpancover_controller_command controller dc "${o[@]}" cpancover-run-once
}

recipe_cpancover-start-queue() {
  COVER_DEBUG=1 perl bin/queue minion worker -j 4
}

recipe_cpancover-start-minion() {
  COVER_DEBUG=1 perl bin/queue daemon -l http://\*:30000 -m production
}

recipe_cpancover-add() {
  module="${1:?No module specified}"
  COVER_DEBUG=1 perl bin/queue add "$module"
}

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

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

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