#!/usr/bin/env bash
#
# Description: Control script for krack-build
#
# Homepage: https://codeberg.org/krathalan/krack
# Copyright (C) 2020-2024 Hunter Peavey
# SPDX-License-Identifier: GPL-3.0-or-later

# -----------------------------------------
# -------------- Guidelines ---------------
# -----------------------------------------

# This script follows the Google Shell Style Guide:
# https://google.github.io/styleguide/shell.xml

# This script uses shellcheck: https://www.shellcheck.net/

# See https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -Eeuo pipefail

# -----------------------------------------
# ----------- Program variables -----------
# -----------------------------------------

# Source a lot of common (between krack scripts) variables
# shellcheck disable=SC1091
source /usr/lib/krack/common

# Needed for $MAKECHROOTPKG_DIR and $KRACKCTL_WATCHSTATUS_INTERVAL
# shellcheck disable=SC1091
source /etc/krack/build.conf

# Other
readonly SCRIPT_NAME="${0##*/}"

# -----------------------------------------
# --------------- Functions ---------------
# -----------------------------------------

exit_script_on_failure()
{
  printf "%sError%s: %s\n" "${RED}" "${NC}" "$1" >&2
  exit 1
}

print_info()
{
  printf "%sInfo:%s %s\n" "${WHITE}" "${NC}" "$1"
}

clean_up()
{
  if [[ -d "${tmpdir}" ]]; then
    rm -rf "${tmpdir}"
  fi
}

build_all()
{
  touch "${DATA_DIR}"/packages/*/krack-request-build
  print_info "Full rebuilds requested"
  rm -rf "${DATA_DIR}"/cache/*
  print_info "Package cache cleared"
}

clean_logs()
{
  # find/printf syntax from:
  # https://stackoverflow.com/questions/4561895/how-to-recursively-find-the-latest-modified-file-in-a-directory/4561987#4561987
  mapfile -t newestLogs <<< "$(find "${LOG_DIR}" -type f -printf '%T@ %p\n' | sort -n | tail -"$1" | cut -f2- -d" ")"

  for logfile in "${LOG_DIR}"/*; do
    if [[ ! -d "${logfile}" ]] && [[ "${newestLogs[*]}" != *"${logfile##*/}"* ]]; then
      rm -f "${logfile}"
    fi
  done
}

create_chroot()
{
  [[ -e "${MAKECHROOTPKG_DIR}" ]] &&
    exit_script_on_failure "Something already exists at ${MAKECHROOTPKG_DIR}."

  sudo mkdir -p "${MAKECHROOTPKG_DIR}"
  sudo mkarchroot "${MAKECHROOTPKG_DIR}"/root base-devel ccache
  print_info "Done making ${MAKECHROOTPKG_DIR} makechrootpkg dir."
}

request_build()
{
  shift 

  while [[ $# -gt 0 ]]; do
    [[ ! -e "${MASTER_BUILD_DIR}/$1" ]] &&
      exit_script_on_failure "Cannot find package $1"

    touch "${MASTER_BUILD_DIR}/$1/krack-request-build"
    print_info "Build requested for package $1"
    shift
  done
}

pending_builds()
{
  local listOfPendingBuilds
  local toPrint
  mapfile -t listOfPendingBuilds <<< "$(find -L "${MASTER_BUILD_DIR}" -name "krack-request-build")"

  printf "List of pending package build requests:\n---------------------------------------\n"

  for pendingBuild in "${listOfPendingBuilds[@]}"; do
    # Use variable substitution to isolate name of pkg directory
    toPrint="${pendingBuild%/*}"
    printf "%s\n" "${toPrint##*/}"
  done
}

cancel_all_requests()
{
  local listOfPendingBuilds
  local toPrint
  mapfile -t listOfPendingBuilds <<< "$(find "${MASTER_BUILD_DIR}" -name "krack-request-build")"

  for pendingBuild in "${listOfPendingBuilds[@]}"; do
    rm -f "${pendingBuild}"
    toPrint="${pendingBuild%/*}"
    print_info "Deleted build request for ${toPrint##*/}"
  done
}

failed_builds()
{
  # Variable is non-local for trap "clean_up"
  tmpdir="$(mktemp -d -t "${SCRIPT_NAME}_XXXXXXXX")"
  readonly tmpdir
  local -r tmpfile="${tmpdir}/stats"

  trap "clean_up" INT SIGINT EXIT

  printf "Failed builds:\n--------------\n" >> "${tmpfile}"

  # Check if failed log directory is empty
  if [[ -n "$(find "${FAILED_BUILDS_LOG_DIR}" -maxdepth 0 -empty)" ]]; then
    exit
  fi

  local stepCounter=1

  # List build and date failed
  for failedBuild in "${FAILED_BUILDS_LOG_DIR}"/*; do
    # Get file creation date of log file and make it more readable
    local logCreationDate
    logCreationDate="$(date -d "$(stat -c "%w" "${failedBuild}")" "+%c")"

    printf "%s. %s%s%s (failed at %s%s%s)\n" "${stepCounter}" "${YELLOW}" "${failedBuild}" "${NC}" "${BLUE}" "${logCreationDate}" "${NC}" >> "${tmpfile}"

    stepCounter=$(( stepCounter + 1 ))
  done

  display_output_file "${tmpfile}"
}

list_diffs()
{
  printf "Diffs:\n------\n"

  # Check if diff directory is empty
  if [[ -n "$(find "${GIT_DIFF_DIR}" -maxdepth 0 -empty)" ]]; then
    exit
  fi

  local stepCounter=1

  for diffFile in "${GIT_DIFF_DIR}"/*; do
    # Get file creation date of diff file and make it more readable
    local diffCreationDate
    diffCreationDate="$(date -d "$(stat -c "%w" "${diffFile}")" "+%c")"

    printf "%s. %s%s%s (created at %s%s%s)\n" "${stepCounter}" "${YELLOW}" "${diffFile}" "${NC}" "${BLUE}" "${diffCreationDate}" "${NC}"

    stepCounter=$(( stepCounter + 1 ))
  done
}

print_help()
{
  printf "Command not recognized. Open man page? "
  read -r -p "[y/N] " response

  case "${response}" in
    [yY][eE][sS]|[yY])
      man krackctl
      ;;
  esac
}

review_diffs()
{
  # Check if diff directory is empty
  if [[ -n "$(find "${GIT_DIFF_DIR}" -maxdepth 0 -empty)" ]]; then
    exit
  fi

  if command -v bat &> /dev/null; then
    krackPager="bat"
  else
    krackPager="${PAGER:-less}"
  fi

  for diffFile in "${GIT_DIFF_DIR}"/*; do
    "${krackPager}" "${diffFile}"
  done

  clear
  printf "Finished reviewing. Delete all diffs? "
  read -r -p "[y/N] " response

  case "${response}" in
    [yY][eE][sS]|[yY])
      rm -f "${GIT_DIFF_DIR}"/*
      ;;
  esac
}

status()
{
  local startTimeUnix
  local timeNowUnix
  local dateDiff
  local -r terminalWidth="$(tput cols)"
  local -r terminalWidthPlusColors="$((terminalWidth + 17))"

  [[ ! -d "${STATUS_DIR}" ]] &&
    exit_script_on_failure "Status files not found. Have you ran krack-build yet?"

  # Use a variable to print lines to easily cut output to terminal width
  local toPrint

  # If temp log file exists, e.g. if krack-build is running:
  if [[ -f "$(cat "${STATUS_CURRENT_LOG_FILE}")" ]]; then
    toPrint="$(printf "%sState:%s %s" "${CYAN}" "${NC}" "$(<"${STATUS_STATE_FILE}")")"

    # If build cycle is in state "building"
    if grep -q building "${STATUS_STATE_FILE}"; then
      # Calculate how long state has been "building"
      startTimeUnix="$(<"${STATUS_DATE_BUILD_START_FILE}")"
      timeNowUnix="$(date +%s)"
      dateDiff="$(( timeNowUnix - startTimeUnix ))"
      toPrint="$(printf "%s, running for %s" "${toPrint}" "$(date -d@"${dateDiff}" -u +%Hh:%Mm:%Ss)")"
    fi

    cut -c 1-"${terminalWidthPlusColors}" <<< "${toPrint}"
  else
    # Otherwise, if krack-build is not running, print time until next trigger
    local nextTrigger
    nextTrigger="$(systemctl status krack-build@builder.timer | grep -i "Trigger:")"
    nextTrigger="${nextTrigger##*Trigger: }"
    printf "%sState:%s sleeping until next rebuild at %s" "${CYAN}" "${NC}" "${nextTrigger}" | cut -c 1-"${terminalWidthPlusColors}"
  fi

  if grep -q building "${STATUS_STATE_FILE}"; then
    toPrint="$(printf "%sSystem stats:%s loadavg %s" "${CYAN}" "${NC}" "$(cut -d' ' -f1-3 < /proc/loadavg)")"

    # Determine CPU temperature (intel/amd)
    if command -v sensors &> /dev/null; then
      local cpuTemp
      local sensorsOutput
      sensorsOutput="$(sensors)"
      
      if printf "%s" "${sensorsOutput}" | grep -q Package; then
        cpuTemp="$(printf "%s" "${sensorsOutput}" | grep Package | cut -d' ' -f5)"
      elif printf "%s" "${sensorsOutput}" | grep -q Tdie; then
        cpuTemp="$(printf "%s" "${sensorsOutput}" | grep Tdie | cut -d' ' -f10)"
        if printf "%s" "${sensorsOutput}" | grep -q SVI2_P_Core; then
          local wattages
          mapfile -t wattages <<< "$(printf "%s" "${sensorsOutput}" | grep _P_ | sed 's/  */ /g' | cut -d' ' -f2)"
          readonly wattages
          local -r totalWattage="$(printf "scale=2; %s + %s\n" "${wattages[0]}" "${wattages[1]}" | bc -l )"

          cpuTemp="${cpuTemp}; ${totalWattage} W"
        fi
      fi

      toPrint="$(printf "%s; CPU temp %s" "${toPrint}" "${cpuTemp}")"
    fi

    cut -c 1-"${terminalWidthPlusColors}" <<< "${toPrint}"

    toPrint="$(printf "%sSubstate:%s %s" "${CYAN}" "${NC}" "$(<"${STATUS_SUBSTATE_FILE}")")"
    
    if grep -q making "${STATUS_SUBSTATE_FILE}"; then
      # Calculate how long state has been "making" and report last build time
      startTimeUnix="$(<"${STATUS_DATE_MAKE_START_FILE}")"
      dateDiff="$(( timeNowUnix - startTimeUnix ))"

      currentPackage="$(cut -d' ' -f2 "${STATUS_SUBSTATE_FILE}")"
      currentPackage="${currentPackage%%,}"
      if [[ -f "${MASTER_BUILD_DIR}/${currentPackage}/.krack-data.json" ]]; then
        local averageBuildTime
        averageBuildTime="$(jq -r .average_build_time "${MASTER_BUILD_DIR}/${currentPackage}/.krack-data.json")"
        if [[ "${averageBuildTime}" == "" ]]; then
          averageBuildTime="time unknown"
        else
          averageBuildTime="time $(date -d@"${averageBuildTime}" -u +%Hh:%Mm:%Ss)"
        fi
      else
        averageBuildTime="time unknown"
      fi
      
      toPrint="$(printf "%s, running for %s (avg build %s)" "${toPrint}" "$(date -d@${dateDiff} -u +%Hh:%Mm:%Ss)" "${averageBuildTime}")"
    fi

    cut -c 1-"${terminalWidthPlusColors}" <<< "${toPrint}"
  fi
  
  printf "%sLatest important messages, from most to least recent:%s\n%s\n" "${CYAN}" "${NC}" "$(tail -n8 "${STATUS_IMPORTANT_MESSAGE_FILE}" | tac | cut -c 1-"${terminalWidth}")"

  # If temp log file exists, e.g. if krack-build is running:
  if [[ -f "$(cat "${STATUS_CURRENT_LOG_FILE}")" ]]; then
    # If user specified how many lines of log to show, use that
    # otherwise default to 10
    if [[ $# -gt 1 ]]; then
      shift
      local -r logLinesToWatch="$1"
    else
      local -r logLinesToWatch="10"
    fi

    printf "%sLatest log entries:%s\n%s\n" "${CYAN}" "${NC}" "$(tail -n"${logLinesToWatch}" "$(<"${STATUS_CURRENT_LOG_FILE}")" | cut -c 1-"${terminalWidth}")"
  else
    # Otherwise, if krack-build is not running, print the last report
    printf "%sLast build report:%s\n" "${CYAN}" "${NC}"
    cat "${STATUS_REPORT_FILE}"
  fi
}

# The same as status(), but uses display_output_file to display output nicely
# Used for `krackctl status` but not `krackctl watch-status`
status_wrapper()
{
  # Variable is non-local for trap "clean_up"
  tmpdir="$(mktemp -d -t "${SCRIPT_NAME}_XXXXXXXX")"
  readonly tmpdir
  local -r tmpfile="${tmpdir}/status-wrapper"

  trap "clean_up" INT SIGINT EXIT

  status > "${tmpfile}"

  display_output_file "${tmpfile}"
}

version()
{
  printf "%s" "\
krack v${VERSION}

Copyright (C) 2020-2024 Hunter Peavey

    Krack is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Krack is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Krack.  If not, see <https://www.gnu.org/licenses/>.
"
}

watch_status()
{
  # Variable is non-local for trap "clean_up"
  tmpdir="$(mktemp -d -t "${SCRIPT_NAME}_XXXXXXXX")"
  readonly tmpdir
  local -r tmpfile="${tmpdir}/watch-status"

  trap "clean_up" INT SIGINT EXIT

  # To fill up the terminal completely and without cutting anything off the top,
  # we need to figure out (1) how many lines are available, and (2) how many lines
  # are being printed by the "latest important messages" section
  local terminalHeight
  terminalHeight=$(tput lines)
  local -r baseLinesUsed="7" # Lines that are always printed, e.g. "Status: "
  local numImportantMessges
  numImportantMessges="$(wc -l "${STATUS_IMPORTANT_MESSAGE_FILE}" | cut -d' ' -f1)"

  # Max important messages displayed is 8
  if [[ "${numImportantMessges}" -gt 8 ]]; then
    numImportantMessges=8
  fi

  local log_display=$(( terminalHeight - baseLinesUsed - numImportantMessges ))

  while true; do
    status "status" "${log_display}" > "${tmpfile}"
    clear
    cat "${tmpfile}"
    # Recalculate lines 
    numImportantMessges="$(wc -l "${STATUS_IMPORTANT_MESSAGE_FILE}" | cut -d' ' -f1)"

    # Max important messages displayed is 8
    if [[ "${numImportantMessges}" -gt 8 ]]; then
      numImportantMessges=8
    fi

    local log_display=$(( terminalHeight - baseLinesUsed - numImportantMessges ))
    sleep "${KRACKCTL_WATCHSTATUS_INTERVAL}"
    terminalHeight=$(tput lines)
  done
}

clear_all_stats()
{
  # NYI
  true
}

# $@: packages to print stats of
# If no arguments, print stats for all packages
print_stats()
{
  # Variable is non-local for trap "clean_up"
  tmpdir="$(mktemp -d -t "${SCRIPT_NAME}_XXXXXXXX")"
  readonly tmpdir
  local -r tmpfile="${tmpdir}/stats"

  trap "clean_up" INT SIGINT EXIT

  shift

  # If no arguments, request all stats
  if [[ $# -eq 0 ]]; then
    for package in "${MASTER_BUILD_DIR}"/*; do
      print_stats_helper "${package}" >> "${tmpfile}"
    done

    display_output_file "${tmpfile}"
    return
  fi

  # If arguments, request stats for specified packages
  while [[ $# -gt 0 ]]; do
    if [[ ! -d "${MASTER_BUILD_DIR}/$1" ]]; then
      printf "Package %s not found\n" "$1" >> "${tmpfile}"
      return
    fi

    print_stats_helper "${MASTER_BUILD_DIR}/$1" >> "${tmpfile}"
    shift
  done
  
  display_output_file "${tmpfile}"
}

# $1: package directory
print_stats_helper()
{
  if [[ ! -f "$1/.krack-data.json" ]]; then
    return
  fi

  mapfile -t packageData <<< "$(jq -r ".average_build_time, .last_build_time, .last_built_version, .build_failure, .number_of_times_built" "$1/.krack-data.json")"
  
  printf "## %s ##\n" "${1##*/}"

  if [[ ! "${packageData[0]:-}" == "" ]]; then
    printf "Average build time: %s\n" "$(date -d@"${packageData[0]}" -u +%Hh:%Mm:%Ss)"
  fi
  
  if [[ ! "${packageData[1]:-}" == "" ]]; then
    printf "Last build time: %s\n" "$(date -d@"${packageData[1]}" -u +%Hh:%Mm:%Ss)"
  fi

  printf "Last built version: %s\n" "${packageData[2]:-}"

  if [[ ! "${packageData[3]:-}" == "" ]]; then
    printf "Build failure: %s\n" "${packageData[3]}"
  fi

  printf "Number of times built: %s\n\n" "${packageData[4]:-0}"
}

# $1: path of file to output
display_output_file()
{
  # Decide how to display output:
  # format text to wrap on words nicely;
  # only use pager if output is longer than terminal height.

  local -r formattedFile="$(fold -w "$(tput cols)" -s "$1")"

  local linesInFile
  mapfile -tn 0 linesInFile <<< "${formattedFile}"
  local -r lengthOfOutput="${#linesInFile[@]}"
  local -r terminalHeight=$(( $(tput lines) - 4 ))
  local krackPager
  
  if [[ "${lengthOfOutput}" -lt "${terminalHeight}" ]] || [[ "${lengthOfOutput}" -eq "${terminalHeight}" ]]; then
    printf "%s" "${formattedFile}"
  else
    krackPager="${PAGER:-less}"

    # Ensure less displays colors properly
    if [[ "${krackPager}" == "less" ]]; then
      krackPager="${krackPager} -R"
    fi

    printf "%s" "${formattedFile}" | ${krackPager}
  fi

  printf "\n"
}

# -----------------------------------------
# ---------------- Script -----------------
# -----------------------------------------

[[ $# -lt 1 ]] && 
  print_help

[[ "$1" != "create-chroot" ]] && [[ "$(whoami)" == "root" ]] &&
  exit_script_on_failure "This script should NOT be run as root (or sudo)!"

# These commands require another argument
if [[ "$1" == "request-build" ]] && [[ $# -lt 2 ]]; then
  exit_script_on_failure "Please specify a package to request a build of."
elif [[ "$1" == "clean-logs" ]] && [[ $# -lt 2 ]]; then
  exit_script_on_failure "Please specify the number of latest logs to keep (e.g. 10)."
fi

case "$1" in
  build-all) build_all ;;
  clean-logs) clean_logs "$2" ;;
  create-chroot) create_chroot ;;
  failed-builds) failed_builds ;;
  list-diffs) list_diffs ;;
  review-diffs) review_diffs ;;
  stats) print_stats "$@" ;;
  request-build) request_build "$@" ;;
  pending-builds) pending_builds ;;
  cancel-all-requests) cancel_all_requests ;;
  status) status_wrapper ;;
  watch-status) watch_status ;;
  version) version ;;
  *) print_help ;;
esac
