#!/usr/bin/env bash
set -Eeuo pipefail

readonly GIT_USER=git
readonly GIT_GROUP=git
readonly STATE_DIR="${GITOLITE_HOME:-/var/lib/gitolite}"
readonly REPOSITORIES_DIR="${STATE_DIR}/repositories"
readonly HOST_KEY_DIR="${STATE_DIR}/.ssh-host-keys"
readonly RUNTIME_DIR=/run/git-browser-appliance
readonly ADMIN_KEY_FILE="${GITOLITE_ADMIN_KEY_FILE:-/run/secrets/gitolite_admin_key}"
readonly ADMIN_NAME="${GITOLITE_ADMIN_NAME:-admin}"

log() {
  printf '[git-browser] %s\n' "$*" >&2
}

die() {
  log "error: $*"
  exit 1
}

run_as_git() {
  runuser -u "${GIT_USER}" -- env \
    HOME="${STATE_DIR}" \
    USER="${GIT_USER}" \
    LOGNAME="${GIT_USER}" \
    PATH="${STATE_DIR}/bin:/usr/local/bin:/usr/bin:/bin" \
    "$@"
}

prepare_state_directory() {
  install -d -o "${GIT_USER}" -g "${GIT_GROUP}" -m 0750 "${STATE_DIR}"

  case "${GITOLITE_FIX_PERMISSIONS:-false}" in
    1|true|TRUE|yes|YES)
      log "repairing ownership under ${STATE_DIR}"
      find "${STATE_DIR}" -xdev -path "${HOST_KEY_DIR}" -prune -o \
        -exec chown "${GIT_USER}:${GIT_GROUP}" {} +
      ;;
    0|false|FALSE|no|NO|'') ;;
    *) die "GITOLITE_FIX_PERMISSIONS must be true or false" ;;
  esac

  run_as_git test -w "${STATE_DIR}" || die \
    "${STATE_DIR} is not writable by uid 1000; fix the volume ownership or start once with GITOLITE_FIX_PERMISSIONS=true"

  run_as_git mkdir -p "${STATE_DIR}/bin" "${STATE_DIR}/.ssh"
  chmod 0700 "${STATE_DIR}/.ssh"
  touch "${STATE_DIR}/.ssh/authorized_keys"
  chown "${GIT_USER}:${GIT_GROUP}" "${STATE_DIR}/.ssh/authorized_keys"
  chmod 0600 "${STATE_DIR}/.ssh/authorized_keys"
}

install_gitolite() {
  log "installing Gitolite into persistent state"
  run_as_git /opt/gitolite/install -to "${STATE_DIR}/bin"
}

is_initialized() {
  [[ -d "${REPOSITORIES_DIR}/gitolite-admin.git" && -f "${STATE_DIR}/.gitolite/conf/gitolite.conf" ]]
}

has_partial_state() {
  [[ -e "${REPOSITORIES_DIR}/gitolite-admin.git" || -e "${STATE_DIR}/.gitolite" || -e "${STATE_DIR}/.gitolite.rc" ]]
}

validate_admin_key() {
  [[ "${ADMIN_NAME}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die \
    "GITOLITE_ADMIN_NAME must contain only letters, digits, dot, underscore, and hyphen"
  [[ -f "${ADMIN_KEY_FILE}" && -r "${ADMIN_KEY_FILE}" ]] || die \
    "first boot requires a readable public key at ${ADMIN_KEY_FILE}"
  grep -Eq '^(ssh-(ed25519|rsa)|ecdsa-sha2-nistp(256|384|521)|sk-(ssh-ed25519|ecdsa-sha2-nistp256)@openssh\.com)[[:space:]]' \
    "${ADMIN_KEY_FILE}" || die "${ADMIN_KEY_FILE} does not look like an OpenSSH public key"
  ssh-keygen -l -f "${ADMIN_KEY_FILE}" >/dev/null 2>&1 || die \
    "${ADMIN_KEY_FILE} is not a valid OpenSSH public key"
}

initialize_gitolite() {
  validate_admin_key

  local setup_dir setup_key
  setup_dir="$(mktemp -d /run/gitolite-setup.XXXXXX)"
  setup_key="${setup_dir}/${ADMIN_NAME}.pub"
  install -o "${GIT_USER}" -g "${GIT_GROUP}" -m 0600 "${ADMIN_KEY_FILE}" "${setup_key}"

  log "initializing Gitolite administrator ${ADMIN_NAME}"
  if ! run_as_git "${STATE_DIR}/bin/gitolite" setup -pk "${setup_key}"; then
    rm -rf -- "${setup_dir}"
    die "Gitolite setup failed"
  fi
  rm -rf -- "${setup_dir}"
}

configure_gitolite() {
  if is_initialized; then
    log "existing Gitolite state found; refreshing generated files and hooks"
    run_as_git "${STATE_DIR}/bin/gitolite" setup
    return
  fi
  if has_partial_state; then
    die "partial Gitolite state found in ${STATE_DIR}; restore a complete volume or clear it intentionally"
  fi
  initialize_gitolite
}

prepare_ssh() {
  install -d -o root -g root -m 0700 "${HOST_KEY_DIR}"
  if [[ ! -s "${HOST_KEY_DIR}/ssh_host_ed25519_key" ]]; then
    log "generating persistent ED25519 SSH host key"
    ssh-keygen -q -t ed25519 -N '' -f "${HOST_KEY_DIR}/ssh_host_ed25519_key"
  fi
  if [[ ! -s "${HOST_KEY_DIR}/ssh_host_rsa_key" ]]; then
    log "generating persistent RSA SSH host key"
    ssh-keygen -q -t rsa -b 3072 -N '' -f "${HOST_KEY_DIR}/ssh_host_rsa_key"
  fi
  chown -R root:root "${HOST_KEY_DIR}"
  chmod 0700 "${HOST_KEY_DIR}"
  chmod 0600 "${HOST_KEY_DIR}"/ssh_host_*_key
  chmod 0644 "${HOST_KEY_DIR}"/ssh_host_*_key.pub

  install -d -m 0755 /run/sshd "${RUNTIME_DIR}"
  /usr/sbin/sshd -t -f /etc/ssh/sshd_config
}

start_services() {
  local -a browser_args
  browser_args=(
    -root "${REPOSITORIES_DIR}"
    -listen "${GIT_BROWSER_LISTEN:-0.0.0.0:8080}"
  )
  if [[ -n "${GIT_BROWSER_CLONE_SSH_PREFIX:-}" ]]; then
    browser_args+=(-clone-ssh-prefix "${GIT_BROWSER_CLONE_SSH_PREFIX}")
  fi
  if [[ -n "${GIT_BROWSER_CLONE_HTTPS_PREFIX:-}" ]]; then
    browser_args+=(-clone-https-prefix "${GIT_BROWSER_CLONE_HTTPS_PREFIX}")
  fi
  if [[ -n "${GIT_BROWSER_HIDE:-}" ]]; then
    browser_args+=(-hide "${GIT_BROWSER_HIDE}")
  fi

  log "starting SSH on :22"
  /usr/sbin/sshd -D -e -f /etc/ssh/sshd_config &
  sshd_pid=$!
  printf '%s\n' "${sshd_pid}" >"${RUNTIME_DIR}/sshd.pid"

  log "starting git-browser on ${GIT_BROWSER_LISTEN:-0.0.0.0:8080}"
  run_as_git /usr/local/bin/git-browser "${browser_args[@]}" &
  browser_pid=$!
  printf '%s\n' "${browser_pid}" >"${RUNTIME_DIR}/git-browser.pid"
}

stop_services() {
  local signal="${1:-TERM}"
  trap - TERM INT
  log "stopping services"
  kill -s "${signal}" "${browser_pid:-}" "${sshd_pid:-}" 2>/dev/null || true
  wait "${browser_pid:-}" 2>/dev/null || true
  wait "${sshd_pid:-}" 2>/dev/null || true
}

supervise_services() {
  local status=0 stopped=
  trap 'stop_services TERM; exit 0' TERM INT

  set +e
  wait -n "${sshd_pid}" "${browser_pid}"
  status=$?
  set -e

  if ! kill -0 "${sshd_pid}" 2>/dev/null; then
    stopped=sshd
  elif ! kill -0 "${browser_pid}" 2>/dev/null; then
    stopped=git-browser
  else
    stopped='a service'
  fi
  log "${stopped} exited unexpectedly with status ${status}"
  stop_services TERM
  exit "${status}"
}

main() {
  [[ "${STATE_DIR}" = /var/lib/gitolite ]] || die \
    "GITOLITE_HOME is fixed at /var/lib/gitolite in this image"
  prepare_state_directory
  install_gitolite
  configure_gitolite
  prepare_ssh
  start_services
  supervise_services
}

main "$@"