#!/bin/sh

set -eu

short_options='f'
long_options='force'

base=$(readlink -f $(dirname $(readlink -f $0))/..)
. $base/lib/environment.sh
. $base/lib/functions.sh

run_id=$(date --rfc-3339=date)

global_tmp_dir=$(mktemp -d)
cleanup() {
  if [ -d "$global_tmp_dir" ]; then
    rm -rf "$global_tmp_dir"
  fi
}
trap cleanup INT TERM EXIT

one_month_ago="${global_tmp_dir}/one_month_ago"
touch -d '1 month ago' "${one_month_ago}"

process_package() {
  # setup tmp dir
  tmp_dir="$global_tmp_dir/$pkg"
  mkdir "$tmp_dir"

  # setup status "DB"
  status_dir=$(status_dir_for_package "$pkg")
  mkdir -p "$status_dir"

  last_status=$($debci_bin_dir/debci-status "$pkg")

  log_file="${status_dir}/${run_id}.log"
  status_file="${status_dir}/${run_id}.json"

  # process packages at most once a day -- except if the first attempt failed
  # for some external reason
  if [ -f "$status_file" ]; then
    if [ "$last_status" = 'tmpfail' ]; then
      # find a new run id
      i=1
      local orig_run_id="$run_id"
      while [ -f "$status_file" ]; do
        run_id="${orig_run_id}-${i}"
        log_file="${status_dir}/${run_id}.log"
        status_file="${status_dir}/${run_id}.json"
        i=$(($i + 1))
      done
    else
      report_status 'skip'
      return 0 # no need to check for this package
    fi
  fi

  do_process_package > "$log_file" 2>&1
  report_status "${status:-skip}"

  if [ -s "$log_file" ]; then
    ln -sf "${run_id}.log" "${status_dir}/latest.log"
  fi
}

report_status() {
  local status="$1"
  if [ -t 1 ]; then
    case "$status" in
      skip)
        color=8
        ;;
      pass)
        color=2
        ;;
      fail)
        color=1
        ;;
      tmpfail)
        color=3
        ;;
      *)
        color=5 # should never get here though
        ;;
    esac
    log "${pkg} \033[38;5;${color}m${status}\033[m"
  else
    log "$pkg" "$status"
  fi
}

do_process_package() {
  if needs_processing; then
    check_platform
    check_base_system

    banner "Started: $(date -R)"

    start_time=$(date +%s)

    set +e
    test-package "$pkg"
    exit_status=$?
    set -e

    # ref: adt-run(1)
    case "$exit_status" in
      0)
        status=pass
        message='All tests passed'
        ;;
      2)
        status=pass
        message='Tests passed, but at least one test skipped'
        ;;
      4)
        status=fail
        message='Tests failed'
        ;;
      6)
        status=fail
        message='Tests failed, and at least one test skipped'
        ;;
      *)
        status=tmpfail
        message='Could not run tests due to a temporary failure'
        ;;
    esac
    finish_time=$(date +%s)
    duration=$(($finish_time - $start_time))

    check_blame

    banner "Finished: $(date -R)"
    echo "∙ Status: $status ($message)"
    record_status
  fi
}

has_reasons_to_run=''
reason_for_run() {
  local reason="$*"
  if [ ! $has_reasons_to_run ]; then
    banner "Triggers for test run"
  fi
  echo "$reason"
  has_reasons_to_run=true
}

# XXX this function must not output anything unless there is a need for the
# package to be processed.
needs_processing() {
  list-dependencies "$pkg" > "$tmp_dir/dependencies.txt"

  run=1

  if [ "$last_status" = 'tmpfail' ]; then
    run=0
    reason_for_run "∙ Retrying run since last attempt failed"
  fi

  if [ -n "$cmdline_force" ]; then
    run=0
    reason_for_run "∙ Forced test run for $pkg"
  fi

  if [ -f "${status_dir}/latest.json" -a "${status_dir}/latest.json" -ot "${one_month_ago}" ]; then
    run=0
    reason_for_run '∙ Forcing test run after 1 month without one'
  fi

  if [ -f "$status_dir/dependencies.txt" ]; then
    if diff -u --label last-run/dependencies.txt "$status_dir/dependencies.txt" --label current-run/dependencies.txt "$tmp_dir/dependencies.txt" > "$tmp_dir/dependencies.diff"; then
      : # no need to run tests
    else
      run=0
      reason_for_run "∙ There were changes in the dependency chain since last test run"
      banner "Change in dependency chain for $pkg since last test run"
      cat "$tmp_dir/dependencies.diff"
    fi
  else
    run=0
    reason_for_run "∙ First test run for $pkg"
  fi

  if [ "$run" -eq 0 ]; then
    cp "$tmp_dir/dependencies.txt" "${status_dir}/dependencies.txt"
    banner "Full dependency chain for $pkg at the moment"
    cat "$status_dir/dependencies.txt"
  fi

  return $run
}

check_base_system() {

  if command_available list-base-system; then
    list-base-system > "${tmp_dir}/base.txt"
  else
    return 0
  fi

  if [ -f "${status_dir}/base.txt" ]; then
    if diff -u \
      --label previous-run/base.txt "${status_dir}/base.txt" \
      --label current-run/base.txt "${tmp_dir}/base.txt" \
        > "${tmp_dir}/base.diff"; then
      :
    else
      cp "${tmp_dir}/base.txt" "${status_dir}/base.txt"
      banner "Change in the base system since last run"
      cat "${tmp_dir}/base.diff"
    fi
  else
    cp "${tmp_dir}/base.txt" "${status_dir}/base.txt"
  fi

  banner "Base system"
  cat "${status_dir}/base.txt"
}

check_platform() {
    banner "Platform information"
    echo "Package versions:"
    dpkg-query --show debci autopkgtest | indent
    echo "Backend: $debci_backend"
}

check_blame() {
  blame="$(debci-status --field blame --json "$pkg")"
  if [ "$blame" = 'unknown' ]; then
    blame='[]'
  fi

  diff="${tmp_dir}/dependencies.diff"
  previous_diff="${status_dir}/dependencies.diff"

  if [ ! -e "$diff" ]; then
    return
  fi

  case "$status" in
    pass)
      blame='[]'
      ;;
    fail)
      case "${last_status}" in
        pass)
          # identify the packages to be blamed
          blame="$($base/scripts/blame "${diff}" "$pkg")"
          ;;
        fail)
          # update versions from the blamed packages, but not include new
          # packages in the blame list. the file pointed to by $previous_diff
          # is guaranteed to exit at this point
          if [ -e "${previous_diff}" ]; then
            blamed_pkgs="$(debci-status --field blame "$pkg" | awk '{print($1)}')"
            combinediff "${previous_diff}" "${diff}" > "${diff}.new"
            mv "${diff}.new" "${diff}"
            blame=$($base/scripts/blame "${diff}" "$pkg" $blamed_pkgs)
          fi
          ;;
      esac
      ;;
  esac

  if [ -f "${diff}" ]; then
    # record dependency chain diff from now to be used in future runs
    cp "${diff}" "${previous_diff}"
  fi
}

record_status() {
  version=$(check_version "$pkg")

  hours=$(( $duration / 3600 ))
  minutes=$(( ($duration % 3600) / 60 ))
  seconds=$(( $duration - ($hours * 3600) - ($minutes * 60) ))
  duration_human="${hours}h ${minutes}m ${seconds}s"

  echo "∙ Duration: ${duration_human}"

  local previous_status="${last_status}"
  if [ "${previous_status}" = 'tmpfail' ]; then
    previous_status=$(debci-status --field previous_status "$pkg")
  fi

  # latest entry
  cat > "${status_file}" <<EOF
{
  "run_id": "${run_id}",
  "package": "${pkg}",
  "version": "${version}",
  "date": "$(date --rfc-3339=seconds)",
  "status": "${status}",
  "blame": $blame,
  "previous_status": "${previous_status}",
  "duration_seconds": "${duration}",
  "duration_human": "${duration_human}",
  "message": "${message}"
}
EOF
  ln -sf "${run_id}.json" "$status_dir/latest.json"

  # TODO cleanup old entries (?)

  # history
  history_file="$tmp_dir/history.json"
  echo '[' > "$history_file"
  sep=''
  entries=$(
    find "$status_dir" -name '*.json' \
      -and -not -name latest.json \
      -and -not -name history.json \
      | sort -Vr
  )
  for entry in $entries; do
    if [ -n "$sep" ]; then
      echo "$sep" >> "$history_file"
    fi
    sep=,
    cat $entry >> "$history_file"
  done
  echo ']' >> "$history_file"
  cp "$history_file" "$status_dir/history.json"
}

# defaults
cmdline_force=''

while true; do
  case "$1" in
    -f|--force)
      shift
      cmdline_force="yes"
      ;;
    --)
      shift
      break
      ;;
    *)
      shift
      ;;
  esac
done

if [ $# -eq 1 ]; then
  pkg="$1"
  process_package
else
  echo "usage: $0 [OPTIONS] PACKAGE"
  exit 1
fi
