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

VERSION="0.0.2"

UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
USER_FILE="${UMBREL_ROOT}/db/user.json"
DEFAULT_UMBREL_APP_REPO_URL="https://github.com/getumbrel/umbrel-apps.git"

show_help() {
  cat << EOF
CLI (v${VERSION}) for managing local Umbrel App Repos

Usage: repo <command> <arguments...>

Commands:
  add                         Add app repo
  remove                      Remove app repo
  update                      Update local app repos
  branch                      Set the branch for a given local app repo
  checkout                    Checkout an app repo (branch is optional) e.g. https://github.com/coolapp/umbrel-apps.git#staging
  path                        Get the absolute path for a given local app repo
    
EOF
}

check_dependencies () {
  for cmd in "$@"; do
    if ! command -v $cmd >/dev/null 2>&1; then
      echo "This script requires \"${cmd}\" to be installed"
      exit 1
    fi
  done
}

# Check dependencies
check_dependencies git jq timeout

if [ -z ${1+x} ]; then
  command=""
else
  command="$1"
fi

USER_JSON=$(cat "${USER_FILE}" 2> /dev/null || echo "null")

function repo_slug() {
  echo "${1}" | sed 's/[^a-zA-Z0-9]/-/g'
}

function repo_path() {
  path=$(repo_slug "${1}")
  echo "${UMBREL_ROOT}/repos/${path}"
}

function list_repos() {
  cat "${USER_FILE}" 2> /dev/null | jq -r 'if has("repos") then .repos else [] end | join("\n")' || {
    echo "${DEFAULT_UMBREL_APP_REPO_URL}"
  }
}

function parse_repo_url() {
  local -r descriptor="${1}"

  # Return the string before the # character
  repo_url="${descriptor%%#*}"

  # Check whether this looks like a url searching for '://' or '@'
  if [[ "${repo_url}" == *://* ]] || [[ "${repo_url}" == *@* ]]; then
    echo "${repo_url}"
    exit
  fi

  # And if not, default to github.com via HTTPS
  echo "https://github.com/${repo_url}"
}

function list_repo_ids() {
  local -r ignore_repo_url="${1}"

  # Check if a repo already exists with this id
  all_repo_urls=$(list_repos)

  # Through existing repos
  for repo_url in $all_repo_urls; do
    # Ignore a given repo url
    if [[ "${repo_url}" != "${ignore_repo_url}" ]]; then
      local_repo_path=$(repo_path "${repo_url}")
      repo_manifest_file="${local_repo_path}/umbrel-app-store.yml"

      # Check a manifest file exists
      if [[ -f "${repo_manifest_file}" ]]; then
        local_repo_id=$(cat "${repo_manifest_file}" | yq '.id')

        echo "${local_repo_id}"
      fi
    fi
  done
}

# Add a new app repo
if [[ "$command" = "add" ]]; then
  descriptor="${2-}"

  if [[ -z "${descriptor}" ]]; then
    >&2 echo "A valid remote repo url must be set. e.g. ${DEFAULT_UMBREL_APP_REPO_URL}"
    exit 1
  fi

  repo_url=$(parse_repo_url "${descriptor}")

  has_repo=$(cat "${USER_FILE}" | jq ".repos | any(. == \"${repo_url}\")")

  if [[ "${has_repo}" == "true" ]]; then
    >&2 echo "Repo: ${repo_url} already exists."
    exit
  fi

  while ! (set -o noclobber; echo "$$" > "${USER_FILE}.lock") 2> /dev/null; do
    echo "Waiting for JSON lock to be released for ${app} update..."
    sleep 1
  done
  # This will cause the lock-file to be deleted in case of a
  # premature exit.
  trap "rm -f "${USER_FILE}.lock"; exit $?" INT TERM EXIT

  updated_json=$(cat "${USER_FILE}" | jq ".repos |= (. + [\"${repo_url}\"] | unique)")
  echo "${updated_json}" > "${USER_FILE}"

  rm -f "${USER_FILE}.lock"

  echo "Added: ${repo_url}"

  exit
fi

# Remove an app repo
if [[ "$command" = "remove" ]]; then
  repo_url="${2-}"

  if [[ -z "${repo_url}" ]]; then
    >&2 echo "A valid remote repo url must be set. e.g. ${DEFAULT_UMBREL_APP_REPO_URL}"
    exit 1
  fi

  has_repo=$(cat "${USER_FILE}" | jq ".repos | any(. == \"${repo_url}\")")

  if [[ "${has_repo}" == "false" ]]; then
    >&2 echo "Repo: ${repo_url} does not exist locally."
    exit
  fi

  while ! (set -o noclobber; echo "$$" > "${USER_FILE}.lock") 2> /dev/null; do
    echo "Waiting for JSON lock to be released for ${app} update..."
    sleep 1
  done
  # This will cause the lock-file to be deleted in case of a
  # premature exit.
  trap "rm -f "${USER_FILE}.lock"; exit $?" INT TERM EXIT

  updated_json=$(cat "${USER_FILE}" | jq ".repos |= (. - [\"${repo_url}\"] | unique)")
  echo "${updated_json}" > "${USER_FILE}"

  rm -f "${USER_FILE}.lock"

  local_repo_path=$(repo_path "${repo_url}")
  rm -rf "${local_repo_path}"

  echo "Removed: ${repo_url}"

  exit
fi

# Update local copies of repos from remote
if [[ "$command" = "update" ]]; then
  # Check if an argument exists to 
  # only update a specific repo
  repo_url="${2-}"
  if [[ ! -z "${repo_url}" ]]; then
    all_repo_urls="${repo_url}"
  else
    all_repo_urls=$(list_repos)
  fi

  for repo_url in $all_repo_urls; do
    echo "Processing: ${repo_url}"

    local_repo_path=$(repo_path "${repo_url}")

    # Check the local app repo is marked as safe by Git
    if ! git config --global --get safe.directory | grep --quiet "^${local_repo_path}$"; then
      git config --global --add safe.directory "${local_repo_path}"
    fi

    # Check integrity of local app repo
    if [[ -d "${local_repo_path}" ]]; then
      set +e
      out=$(git -C "${local_repo_path}" status 2>&1)
      # 1 = network error (and other errors)
      # 128 = git repo bad/corrupt/incomplete
      exit_code=$?
      set -e
      if [[ "${exit_code}" == "128" ]]; then
        echo "${out}"
        echo
        echo "Bad local app repo detected. Removing..."
        
        rm -rf "${local_repo_path}"
      fi
    fi

    # Ignore exit code just in case 1 repo is bad
    # Which would then prevent other repos getting updated
    {
      if [[ -d "${local_repo_path}" ]]; then
        # Git can leave behind write locks if something fails
        # These locks should only exist for milliseconds
        # We'll delete any lock  older than 10 seconds
        # Otherwise git will not proceed
        epoch_ts=$(date +%s)
        for file in "${local_repo_path}/.git"/*.lock; do
          [[ ! -f "${file}" ]] && continue

          age_of_lock=$(stat -c '%X' "${file}")
          age_seconds=$((epoch_ts-age_of_lock))

          if [[ "${age_seconds}" -gt "10" ]]; then
            echo "Deleting old lock: ${file}"
            rm -rf "${file}"
          fi
        done

        repo_branch=$(git -C "${local_repo_path}" rev-parse --abbrev-ref HEAD)
        echo "Updating repo: ${repo_url} (${repo_branch})"

        timeout --foreground 30 git -C "${local_repo_path}" pull
      else
        echo "Cloning repo: ${repo_url}"

        timeout --foreground 30 git clone "${repo_url}" "${local_repo_path}"

        # Check we have a valid app repo
        if [[ "${repo_url}" != "${DEFAULT_UMBREL_APP_REPO_URL}" ]]; then
          repo_manifest_file="${local_repo_path}/umbrel-app-store.yml"

          if [[ -f "${repo_manifest_file}" ]]; then
            local_repo_id=$(cat "${repo_manifest_file}" | yq '.id')

            ids=$(list_repo_ids "${repo_url}")
            if echo "${ids}" | grep --quiet "^${local_repo_id}$"; then
              echo
              echo "Error: Repo (${local_repo_id}) already exists"
              echo
              "${0}" "remove" "${repo_url}"
            fi
          else
            echo
            echo "Error: Repo (${repo_url}) missing umbrel-app-store.yml"
            echo
            "${0}" "remove" "${repo_url}"
          fi
        fi
      fi
    } || true

    # Make sure app repo doesn't end up being owned by root or some apps will fail
    # with fs permission errors
    if [[ -d "${local_repo_path}" ]]; then
      chown -R "1000:1000" "${local_repo_path}"
    fi
    
    echo
  done

  exit
fi

if [[ "$command" = "branch" ]]; then
  repo="${2-}"
  branch="${3-}"

  if [[ -z "${repo}" ]]; then
    >&2 echo "A valid repo must be given. e.g. ${DEFAULT_UMBREL_APP_REPO_URL}"
    exit 1
  fi

  if [[ -z "${branch}" ]]; then
    >&2 echo "A valid branch name must be given. e.g. staging"
    exit 1
  fi

  local_repo_path=$(repo_path "${repo}")

  if [[ ! -d "${local_repo_path}" ]]; then
    >&2 echo "App repo does not yet exist locally"
    >&2 echo "Please use the 'update' subcommand"
    exit 1
  fi

  # Check whether it's only fetching for master
  git_config_file="${local_repo_path}/.git/config"
  if grep -q "refs/heads/master:refs/remotes/origin/master" "${git_config_file}"; then
    sed -i "s/refs\/heads\/master:refs\/remotes\/origin\/master/refs\/heads\/*:refs\/remotes\/origin\/*/g;" "${git_config_file}"
  fi

  # Fetch remote commits, files, and refs
  git -C "${local_repo_path}" fetch

  # Checkout branch
  git -C "${local_repo_path}" checkout "${branch}"

  # Update local repo
  "${0}" "update" "${repo}"

  exit
fi

if [[ "$command" = "checkout" ]]; then
  descriptor="${2-}"

  if [[ -z "${descriptor}" ]]; then
    >&2 echo "Error: A valid remote git repo descriptor must be given. e.g. coolapp/umbrel-apps"
    exit 1
  fi

  # These are all valid repo descriptors
  # - http://github.com/coolapp/umbrel-apps
  # - coolapp/umbrel-apps (will use github.com via https)
  # - coolapp/umbrel-apps#staging

  # Note: using the .git extension is optional
  repo=$(parse_repo_url "${descriptor}")

  branch="master"
  if [[ "${descriptor}" == *"#"* ]]; then
    branch="${descriptor#*#}"
  fi

  default_repo_path=$(repo_path "${DEFAULT_UMBREL_APP_REPO_URL}")

  if [[ ! -d "${default_repo_path}" ]]; then
    >&2 echo "Error: The default Umbrel app repo must be setup first."
    exit 1
  fi

  if [[ "${repo}" == "${DEFAULT_UMBREL_APP_REPO_URL}" ]]; then
    origin_name="origin"
  else
    origin_name=$(printf "%s" "${repo}" | openssl dgst -sha256 | sed 's/^.* //')
  fi

  if ! git -C "${default_repo_path}" remote | grep "${origin_name}" > /dev/null; then
    git -C "${default_repo_path}" remote add "${origin_name}" "${repo}"
  fi  

  git -C "${default_repo_path}" fetch "${origin_name}" "${branch}"

  git -C "${default_repo_path}" checkout "${origin_name}/${branch}"

  # Make sure app repo doesn't end up being owned by root or some apps will fail
  # with fs permission errors
  if [[ -d "${default_repo_path}" ]]; then
    chown -R "1000:1000" "${default_repo_path}"
  fi

  exit
fi

if [[ "$command" = "path" ]]; then
  repo_url="${2-}"

  repo_path "${repo_url}"

  exit
fi

if [[ "$command" = "locate" ]]; then
  app="${2-}"

  all_repo_urls=$(list_repos)
  for repo_url in $all_repo_urls; do
    local_repo_path=$(repo_path "${repo_url}")

    if [[ -d "${local_repo_path}/${app}" ]]; then
      echo "${repo_url}"
      exit
    fi
  done

  exit
fi

# If we get here it means no valid command was supplied
# Show help and exit
show_help
exit 1
