#!/bin/bash
# rosaclone - clone projects from ROSA git hosting

# list of config variables (env > config, CLI keys override both)
config_vars="GIT_PROTO ABF_URL_HTTPS ABF_URL_SSH ABF_OWNER ABF_PLATFORM \
FAB_URL_HTTPS FAB_URL_SSH FAB_OWNER FAB_PLATFORM FAB_BRANCHES \
DEFAULT_SOURCE BASE_DIR"

# save env values for config vars BEFORE defaults touch them
declare -A env_saved
for _var in $config_vars; do
  if [ -n "${!_var+x}" ]; then
    env_saved[$_var]="${!_var}"
  fi
done
unset _var

# ---------------- defaults ----------------

GIT_PROTO="https"
ABF_URL_HTTPS=""
ABF_URL_SSH=""
ABF_OWNER=""
ABF_PLATFORM=""

FAB_URL_HTTPS=""
FAB_URL_SSH=""
FAB_OWNER=""
FAB_PLATFORM=""
FAB_BRANCHES=""

DEFAULT_SOURCE="abf"
BASE_DIR=""

# CLI overrides
opt_abf=0
opt_fab=0
opt_a=0
opt_ssh=0
opt_https=0
opt_force=0
opt_auth=""
opt_dest=""
opt_init_config=0
opt_debug=0

# ---------------- usage ----------------

usage() {
  cat <<'EOT'
usage:
  rosaclone [OPTIONS] <arg>

rosaclone - clone ROSA Linux project git repositories

  The tool clones a project's git repository from the ROSA Linux
  infrastructure and can combine the old ABF (abf.rosa.ru) and the
  new ABF (gitea.rosa.ru) into a single local git working directory.

  After a single clone you can work with old ABF branches
  (through rosa2023.1) and new ABF branches (from rosa14 onward)
  as if they all lived in one git repository.

Each source is registered as a separate git remote:

  origin  - old ABF (abf.rosa.ru, group 'import' by default)
  fab     - new ABF (gitea.rosa.ru, group 'rosalinux' by default)

  This makes it possible to keep using the familiar workflow with
  old platform branches while also working with new platform branches,
  without switching between two separate clones.

modes:
  --abf  | -a    clone from abf only
  --fab  | -f    clone from fab only
  --both | -b    fork mode: origin = abf, remote 'fab' = fab,
                 branches from FAB_BRANCHES are created from fab/<branch>

  if neither -a, -f, -b is given, DEFAULT_SOURCE from config is used
  (abf | fab | both)

<arg> for --abf / --fab:
  full URL              e.g. https://abf.rosa.ru/import/dracut.git
                        e.g. git@abf.rosa.ru:import/dracut.git
  owner/project         e.g. import/dracut
  project               e.g. dracut

<arg> for --both:
  full URL from abf     e.g. https://abf.rosa.ru/import/dracut.git
  full URL from fab     e.g. https://gitea.rosa.ru/rosalinux/dracut.git
  project               e.g. dracut

  owner/project is not allowed for --both

protocol:
  --ssh                 use ssh (ABF_URL_SSH / FAB_URL_SSH)
  --https, --http       use http(s) (ABF_URL_HTTPS / FAB_URL_HTTPS)

  if neither is given, GIT_PROTO from config is used
  --ssh and --https/--http are mutually exclusive

common:
  -F, --force           remove destination dir before clone
                        (only if it is a git repository)
  -d, --dest=DIR        destination base dir (overrides BASE_DIR)
  --auth=USER[:PASS]    only used for http(s):// URLs
  --init-config         regenerate user config from defaults
  --debug               print debug info to stderr
  --help

config:
  /etc/rosaclone/config           (system defaults)
  ~/.config/rosaclone/config      (overrides system)

env:
  any config variable can be overridden by environment,
  e.g. BASE_DIR=/tmp rosaclone ...
EOT
  exit 1
}

# ---------------- logging ----------------

log()   { echo "$*"; }
warn()  { echo "warning: $*" >&2; }
die()   { echo "error: $*" >&2; exit 1; }

debug() {
  if [ "$opt_debug" -gt 0 ]; then
    echo "debug: $*" >&2
  fi
  return 0
}

# ---------------- preflight ----------------

check_git() {
  command -v git >/dev/null 2>&1 || die "git not found in PATH"
  debug "git found: $(command -v git)"
}

# ---------------- config ----------------

ask() {
  local prompt="$1" default="$2" value

  debug "ask: prompt='$prompt' default='$default'"
  debug "ask: [ -t 0 ]=$([ -t 0 ] && echo yes || echo no)"
  debug "ask: [ -r /dev/tty ]=$([ -r /dev/tty ] && echo yes || echo no)"
  debug "ask: [ -w /dev/tty ]=$([ -w /dev/tty ] && echo yes || echo no)"

  if [ -t 0 ]; then
    debug "ask: reading from stdin (terminal)"
    read -r -p "$prompt [$default]: " value
  elif [ -r /dev/tty ] && [ -w /dev/tty ]; then
    debug "ask: reading from /dev/tty"
    read -r -p "$prompt [$default]: " value < /dev/tty
  else
    debug "ask: no tty, using default"
    echo "$default"
    return
  fi

  debug "ask: raw value='$value'"
  echo "${value:-$default}"
}

create_config_interactive() {
  local dir="$HOME/.config/rosaclone"
  mkdir -p "$dir"

  log "creating config: $dir/config"
  debug "create_config_interactive: dir=$dir"

  local git_proto abf_https abf_ssh abf_owner abf_platform
  local fab_https fab_ssh fab_owner fab_platform fab_branches
  local default_source base_dir

  echo '====================================================' 1>&2
  abf_https=$(ask "ABF_URL_HTTPS" "https://abf.rosa.ru/")
  abf_ssh=$(ask "ABF_URL_SSH" "git@abf.rosa.ru:")
  abf_owner=$(ask "ABF_OWNER" "import")
  abf_platform=$(ask "ABF_PLATFORM" "rosa2023.1")
  echo '====================================================' 1>&2
  fab_https=$(ask "FAB_URL_HTTPS" "https://gitea.rosa.ru/")
  fab_ssh=$(ask "FAB_URL_SSH" "ssh://git@gitea.rosa.ru:222/")
  fab_owner=$(ask "FAB_OWNER" "rosalinux")
  fab_platform=$(ask "FAB_PLATFORM" "rosa14")
  fab_branches=$(ask "FAB_BRANCHES" "rosa14")
  echo '====================================================' 1>&2
  git_proto=$(ask "GIT_PROTO" "https")
  default_source=$(ask "DEFAULT_SOURCE (abf|fab|both)" "abf")
  base_dir=$(ask "BASE_DIR" "")
  echo '====================================================' 1>&2

  debug "create_config_interactive: writing $dir/config"

  cat > "$dir/config" <<EOF
# rosaclone config
# default values for ROSA git hosting

GIT_PROTO=$git_proto

ABF_URL_HTTPS=$abf_https
ABF_URL_SSH=$abf_ssh
ABF_OWNER=$abf_owner
ABF_PLATFORM=$abf_platform

FAB_URL_HTTPS=$fab_https
FAB_URL_SSH=$fab_ssh
FAB_OWNER=$fab_owner
FAB_PLATFORM=$fab_platform
FAB_BRANCHES="$fab_branches"

DEFAULT_SOURCE=$default_source
BASE_DIR=$base_dir
EOF
  chmod 644 "$dir/config"
  debug "create_config_interactive: done"
}

load_config() {
  debug "load_config: opt_init_config=$opt_init_config"

  # --init-config: full reset, ignore env and existing config
  if [ "$opt_init_config" -eq 1 ]; then
    rm -f "$HOME/.config/rosaclone/config"
    create_config_interactive
    # shellcheck disable=SC1091
    . "$HOME/.config/rosaclone/config"
    return
  fi

  # source configs (may override vars)
  local config_found=0
  if [ -f /etc/rosaclone/config ]; then
    debug "load_config: sourcing /etc/rosaclone/config"
    # shellcheck disable=SC1091
    . /etc/rosaclone/config
    config_found=1
  fi
  if [ -f "$HOME/.config/rosaclone/config" ]; then
    debug "load_config: sourcing $HOME/.config/rosaclone/config"
    # shellcheck disable=SC1091
    . "$HOME/.config/rosaclone/config"
    config_found=1
  fi
  if [ "$config_found" -eq 0 ]; then
    debug "load_config: no config found, creating"
    create_config_interactive
    # shellcheck disable=SC1091
    . "$HOME/.config/rosaclone/config"
  fi

  # restore env values (env > config)
  local var
  for var in "${!env_saved[@]}"; do
    printf -v "$var" '%s' "${env_saved[$var]}"
    debug "load_config: restored env $var='${env_saved[$var]}'"
  done
}

print_config_vars() {
  [ "$opt_debug" -gt 0 ] || return 0
  debug "resolved config:"
  local var
  for var in $config_vars; do
    debug "  $var='${!var}'"
  done
}

# ---------------- argument parsing ----------------

parse_args() {
  debug "parse_args: argv=$*"
  while [ $# -gt 0 ]; do
    debug "parse_args: arg='$1'"
    case "$1" in
      '--abf'  | '-a' )        opt_abf=1 ;;
      '--fab'  | '-f' )        opt_fab=1 ;;
      '--both' | '-b' )        opt_a=1 ;;
      '--ssh' )                opt_ssh=1 ;;
      '--https' | '--http' )   opt_https=1 ;;
      '-F' | '--force' )       opt_force=1 ;;
      '-d' )
        shift
        [ $# -gt 0 ] || die "-d requires a value"
        opt_dest="$1"
        ;;
      --dest=*)                opt_dest="${1#--dest=}" ;;
      --dest)
        shift
        [ $# -gt 0 ] || die "--dest requires a value"
        opt_dest="$1"
        ;;
      --auth=*)                opt_auth="${1#--auth=}" ;;
      '--init-config' )        opt_init_config=1 ;;
      '--debug' )              opt_debug=1 ;;
      '--help' | '-h')         usage ;;
      -*)
        die "unknown option: $1"
        ;;
      *)
        if [ -n "${ARG:-}" ]; then
          die "only one project argument is supported"
        fi
        ARG="$1"
        ;;
    esac
    shift
  done
  debug "parse_args: ARG='${ARG:-}' opt_abf=$opt_abf opt_fab=$opt_fab opt_a=$opt_a opt_ssh=$opt_ssh opt_https=$opt_https opt_force=$opt_force opt_dest='$opt_dest'"
}

# ---------------- URL helpers ----------------

url_with_auth() {
  local url="$1" auth="$2"
  if [ -z "$auth" ]; then
    echo "$url"
    return
  fi
  case "$url" in
    http://*@*|https://*@*)
      echo "$url"
      ;;
    http://*)
      echo "$url" | sed -E "s|^http://|http://${auth}@|"
      ;;
    https://*)
      echo "$url" | sed -E "s|^https://|https://${auth}@|"
      ;;
    *)
      warn "AUTH is set but URL is not http(s)://, ignoring auth"
      echo "$url"
      ;;
  esac
}

extract_host() {
  local url="$1" host
  case "$url" in
    *://*)
      host=$(echo "$url" | sed -E 's|^[a-z][a-z0-9+.-]*://([^/]+).*$|\1|')
      ;;
    *@*:*)
      host=$(echo "$url" | sed -E 's|^[^@]*@([^:]+):.*$|\1|')
      ;;
    *)
      host=""
      ;;
  esac
  host="${host##*@}"
  echo "$host"
}

extract_owner_from_url() {
  local url="$1" rest
  case "$url" in
    *://*)
      rest="${url#*://}"
      rest="${rest#*/}"
      ;;
    *@*:*)
      rest="${url#*:}"
      ;;
    *)
      rest="$url"
      ;;
  esac
  echo "${rest%%/*}"
}

extract_project_from_url() {
  local url="$1" base
  base=$(basename "$url")
  base="${base%.git}"
  echo "$base"
}

build_url() {
  local host="$1" owner="$2" project="$3"
  local base
  case "$host/$GIT_PROTO" in
    abf/https) base="$ABF_URL_HTTPS" ;;
    abf/ssh)   base="$ABF_URL_SSH" ;;
    fab/https) base="$FAB_URL_HTTPS" ;;
    fab/ssh)   base="$FAB_URL_SSH" ;;
    *)         die "unsupported host/proto: $host/$GIT_PROTO" ;;
  esac
  [ -n "$base" ] || die "URL for $host/$GIT_PROTO is not set in config"
  echo "${base}${owner}/${project}.git"
}

# ---------------- mode validation ----------------

validate_modes() {
  debug "validate_modes: opt_a=$opt_a opt_abf=$opt_abf opt_fab=$opt_fab opt_ssh=$opt_ssh opt_https=$opt_https GIT_PROTO=$GIT_PROTO DEFAULT_SOURCE=$DEFAULT_SOURCE"

  if [ "$opt_a" -eq 1 ] && { [ "$opt_abf" -eq 1 ] || [ "$opt_fab" -eq 1 ]; }; then
    die "-b cannot be combined with --abf or --fab"
  fi

  if [ "$opt_ssh" -eq 1 ] && [ "$opt_https" -eq 1 ]; then
    die "only one of --ssh, --https is allowed"
  fi

  if [ "$opt_ssh" -eq 1 ]; then
    GIT_PROTO=ssh
  elif [ "$opt_https" -eq 1 ]; then
    GIT_PROTO=https
  fi
  debug "validate_modes: GIT_PROTO=$GIT_PROTO"

  if [ "$opt_a" -eq 0 ] && [ "$opt_abf" -eq 0 ] && [ "$opt_fab" -eq 0 ]; then
    case "$DEFAULT_SOURCE" in
      abf)  opt_abf=1 ;;
      fab)  opt_fab=1 ;;
      both) opt_a=1 ;;
      *)
        die "unknown DEFAULT_SOURCE: '$DEFAULT_SOURCE' (expected: abf | fab | both)"
        ;;
    esac
    debug "validate_modes: using DEFAULT_SOURCE=$DEFAULT_SOURCE"
  fi

  case "$GIT_PROTO" in
    https|ssh) ;;
    *) die "unsupported GIT_PROTO: '$GIT_PROTO' (expected: https | ssh)" ;;
  esac

  if [ "$opt_a" -eq 1 ]; then
    [ -n "$ABF_OWNER" ] || die "ABF_OWNER is not set in config"
    [ -n "$FAB_OWNER" ] || die "FAB_OWNER is not set in config"
  elif [ "$opt_fab" -eq 1 ]; then
    [ -n "$FAB_OWNER" ] || die "FAB_OWNER is not set in config"
  else
    [ -n "$ABF_OWNER" ] || die "ABF_OWNER is not set in config"
  fi
}

# ---------------- project arg parsing ----------------

parse_arg_single() {
  local arg="$1" default_owner="$2"
  debug "parse_arg_single: arg='$arg' default_owner='$default_owner'"
  case "$arg" in
    *://*|*@*:*)
      p_owner=$(extract_owner_from_url "$arg")
      p_project=$(extract_project_from_url "$arg")
      ;;
    */*)
      p_owner="${arg%%/*}"
      p_project="${arg#*/}"
      ;;
    *)
      p_owner="$default_owner"
      p_project="$arg"
      ;;
  esac
  debug "parse_arg_single: p_owner='$p_owner' p_project='$p_project'"
  [ -n "$p_owner" ] || die "cannot determine owner from '$arg'"
  [ -n "$p_project" ] || die "cannot determine project from '$arg'"
}

parse_arg_both() {
  local arg="$1"
  debug "parse_arg_both: arg='$arg'"
  case "$arg" in
    *://*|*@*:*)
      local host owner url_pair
      host=$(extract_host "$arg")
      owner=$(extract_owner_from_url "$arg")
      p_project=$(extract_project_from_url "$arg")
      debug "parse_arg_both: host='$host' owner='$owner' project='$p_project'"
      [ -n "$p_project" ] || die "cannot determine project from '$arg'"

      url_pair="$host/$owner"

      local abf_base fab_base abf_host fab_host abf_pair fab_pair
      case "$GIT_PROTO" in
        https) abf_base="$ABF_URL_HTTPS"; fab_base="$FAB_URL_HTTPS" ;;
        ssh)   abf_base="$ABF_URL_SSH";   fab_base="$FAB_URL_SSH" ;;
      esac

      abf_host=$(extract_host "$abf_base")
      fab_host=$(extract_host "$fab_base")
      abf_pair="$abf_host/$ABF_OWNER"
      fab_pair="$fab_host/$FAB_OWNER"

      debug "parse_arg_both: abf_pair='$abf_pair' fab_pair='$fab_pair'"

      if [ "$url_pair" = "$abf_pair" ] || [ "$url_pair" = "$fab_pair" ]; then
        debug "parse_arg_both: host/owner matches: $url_pair"
      else
        die "URL '$arg' does not match '$abf_pair' or '$fab_pair'"
      fi
      ;;
    */*)
      die "-b does not accept owner/project: '$arg'"
      ;;
    *)
      p_project="$arg"
      ;;
  esac
  [ -n "$p_project" ] || die "cannot determine project from '$arg'"
  debug "parse_arg_both: p_project='$p_project'"
}

# ---------------- git operations ----------------

compute_target_dir() {
  local owner="$1" project="$2"
  local base

  if [ -n "$opt_dest" ]; then
    base="$opt_dest"
    debug "compute_target_dir: using -d/--dest: '$base'"
  elif [ -n "$BASE_DIR" ]; then
    base="$BASE_DIR"
    debug "compute_target_dir: using BASE_DIR: '$base'"
  else
    base=""
  fi

  if [ -n "$base" ]; then
    target_dir="$base/$owner/$project"
    mkdir -p "$base/$owner" || die "cannot create $base/$owner"
  else
    target_dir="$project"
  fi

  debug "compute_target_dir: target_dir='$target_dir'"

  if [ "$opt_force" -eq 1 ]; then
    safe_rm_for_clone "$target_dir"
  fi
}

safe_rm_for_clone() {
  local dir="$1"
  local norm basename

  [ -n "$dir" ] || die "--force: refusing to remove empty path"

  norm="${dir%/}"
  [ -n "$norm" ] || norm="/"
  basename="${norm##*/}"

  case "$norm" in
    "/"|"$HOME"|"$(cd "$HOME" 2>/dev/null && pwd)")
      die "--force: refusing to remove '$dir'"
      ;;
  esac

  if [ ! -e "$dir" ]; then
    debug "--force: '$dir' does not exist, nothing to remove"
    return 0
  fi

  if   [ -L "$dir" ]; then
    die "--force: refusing to remove symlink '$dir'"
  elif [ ! -d "$dir" ]; then
    die "--force: '$dir' exists and is not a directory"
  elif [ -z "$basename" ]; then
    die "--force: refusing to remove path with empty basename: '$dir'"
  elif [ "${basename#.}" != "$basename" ]; then
    die "--force: basename '$basename' starts with a dot, refusing to remove"
  elif [ "$basename" != "$(echo "$basename" | tr -d '[:space:]')" ]; then
    die "--force: basename '$basename' contains whitespace, refusing to remove"
  elif printf '%s' "$basename" | LC_ALL=C grep -qE '[^ -~]'; then
    die "--force: basename '$basename' contains non-ASCII characters, refusing to remove"
  elif [ ! -d "$dir/.git" ]; then
    die "--force: '$dir' is not a git repository, refusing to remove"
  fi

  debug "--force: removing '$dir'"
  rm -rf "$dir" || die "--force: cannot remove '$dir'"
}

do_final_checkout() {
  local platform="$1"
  if [ -z "$platform" ]; then
    debug "no platform to checkout, staying on current branch"
    return
  fi
  log "checkout $platform"
  if ! git checkout "$platform" 2>/dev/null; then
    warn "cannot checkout '$platform', staying where we are"
  fi
}

show_branches_info() {
  log ""
  log "remotes:"
  git --no-pager remote -v
  log ""
  log "branches:"

  local patterns=""
  [ -n "$ABF_PLATFORM" ] && patterns="$patterns $ABF_PLATFORM"
  [ -n "$FAB_PLATFORM" ] && patterns="$patterns $FAB_PLATFORM"
  patterns="$patterns $FAB_BRANCHES"

  if [ -z "${patterns// /}" ]; then
    log "  (no branches to show)"
  else
    # shellcheck disable=SC2086
    git --no-pager branch -vv --list $patterns
  fi
  log ""
}


print_project_dir() {
  pwd
}

do_mode_both() {
  local project="$1"

  local abf_url fab_url
  abf_url=$(build_url abf "$ABF_OWNER" "$project")
  abf_url=$(url_with_auth "$abf_url" "$opt_auth")
  fab_url=$(build_url fab "$FAB_OWNER" "$project")

  debug "do_mode_both: abf_url='$abf_url' fab_url='$fab_url'"

  log "cloning $abf_url -> $target_dir"
  git clone "$abf_url" "$target_dir" || die "git clone failed"
  cd "$target_dir" || die "cannot cd into $target_dir"

  # checkout ABF_PLATFORM from origin, unless it is in FAB_BRANCHES
  if [ -n "$ABF_PLATFORM" ]; then
    local skip_abf=0
    local fb
    for fb in $FAB_BRANCHES; do
      [ "$fb" = "$ABF_PLATFORM" ] && skip_abf=1
    done
    if [ "$skip_abf" -eq 0 ]; then
      log "checkout $ABF_PLATFORM (from origin)"
      if ! git checkout "$ABF_PLATFORM"; then
        warn "cannot checkout '$ABF_PLATFORM' from origin, staying where we are"
      fi
    else
      debug "skipping ABF_PLATFORM checkout: '$ABF_PLATFORM' is in FAB_BRANCHES"
    fi
  fi

  log "adding remote 'fab' -> $fab_url"
  git remote add fab "$fab_url" || die "git remote add fab failed"

  git config --unset-all remote.fab.fetch || true
  local branch
  for branch in $FAB_BRANCHES; do
    git config --add remote.fab.fetch "+refs/heads/$branch:refs/remotes/fab/$branch"
  done

  git config checkout.defaultRemote origin

  log "fetching fab"
  git fetch fab || die "git fetch fab failed"

  if [ -z "$FAB_BRANCHES" ]; then
    warn "FAB_BRANCHES is empty, no local branches created"
    do_final_checkout "$FAB_PLATFORM"
    show_branches_info
    print_project_dir
    return
  fi

  for branch in $FAB_BRANCHES; do
    if git show-ref --verify --quiet "refs/remotes/fab/$branch"; then
      log "creating local branch '$branch' from 'fab/$branch'"
      git checkout -b "$branch" "fab/$branch" || {
        warn "cannot create branch '$branch', skipping"
        continue
      }
      git branch --set-upstream-to="fab/$branch" "$branch" >/dev/null 2>&1
    else
      warn "branch '$branch' not found on fab, skipping"
    fi
  done

  do_final_checkout "$FAB_PLATFORM"
  show_branches_info
  print_project_dir
}

do_clone_one() {
  local project="$1" owner="$2"
  local url platform host

  if [ "$opt_fab" -eq 1 ]; then
    host=fab
    platform="$FAB_PLATFORM"
  else
    host=abf
    platform="$ABF_PLATFORM"
  fi

  url=$(build_url "$host" "$owner" "$project")
  url=$(url_with_auth "$url" "$opt_auth")

  debug "do_clone_one: url='$url' platform='$platform'"

  log "cloning $url -> $target_dir"
  git clone "$url" "$target_dir" || die "git clone failed"
  cd "$target_dir" || die "cannot cd into $target_dir"

  do_final_checkout "$platform"
  print_project_dir
}

# ---------------- main ----------------

main() {
  case "${1:-}" in
    -h|--help|help)
      usage
      ;;
  esac

  check_git

  # pre-scan for --debug and --init-config so that load_config can use them
  for a in "$@"; do
    case "$a" in
      --debug)       opt_debug=1 ;;
      --init-config) opt_init_config=1 ;;
    esac
  done

  debug "main: argv=$*"
  debug "main: opt_debug=$opt_debug"

  load_config
  parse_args "$@"
  print_config_vars

  # --init-config alone: config already regenerated in load_config
  if [ "$opt_init_config" -eq 1 ] && [ -z "${ARG:-}" ]; then
    return 0
  fi

  # no project argument: show usage
  if [ -z "${ARG:-}" ]; then
    usage
  fi

  validate_modes

  if [ "$opt_a" -eq 1 ]; then
    parse_arg_both "$ARG"
    compute_target_dir "$ABF_OWNER" "$p_project"
    do_mode_both "$p_project"
  else
    if [ "$opt_fab" -eq 1 ]; then
      parse_arg_single "$ARG" "$FAB_OWNER"
    else
      parse_arg_single "$ARG" "$ABF_OWNER"
    fi
    compute_target_dir "$p_owner" "$p_project"
    do_clone_one "$p_project" "$p_owner"
  fi
}

ARG=""
p_owner=""
p_project=""
target_dir=""

main "$@"
