# internal shortcuts
auth() {
  rlx.commands.auth "$@";
}

admin() {
  rlx.commands.admin "$@";
}

session() {
  rlx.commands.session "$@";
}

login() {
  rlx.commands.login "$@";
}

logout() {
  rlx.commands.logout "$@";
}

# commands shortcuts
rlx.commands.login() {
  auth session login "$@";
}

rlx.commands.logout() {
  auth session logout "$@";
}

rlx.commands.session() {
  auth session get "$@";
}

rlx.commands.whoami() {
  local server="${1:-${info[server]:-}}";
  alias.expand || return 1;
  if [ -z "${server}" ]; then
    console error -- "no server specified";
  else
    local print=false;
    session "${server}" || return 1;
    if ${flags[success]}; then
      rlx.interpreter whoami;
      local parts=( $( "$interpreter" < "${files[response]}" || false ) );
      if [ $? -eq 0 ] && [ ${#parts[@]} -gt 0 ]; then
        local username="${parts[0]}";
        if [ "${username}" == null ]; then
          username="${defaults[username]}";
        fi
        echo "${username}";
      else
        echo "${defaults[username]}";
      fi
    fi
  fi
}

# main
rlx.commands.auth() {
  if [ $# -eq 0 ]; then
    console error -- "no auth command specified";
  else
    local namespace="auth.commands";
    local cmd="${1:-}"; shift;
    if ! method.exists? "${namespace}.${cmd}"; then
      console error -- "unknown auth command %s" "${cmd}";
    else
      delegate "${namespace}" "${cmd}" "$@";
    fi
  fi
}

auth.commands.require() {
  local value="${1:-}";
  local server="${2:-${info[server]}}";
  if [ -z "${server}" ]; then
    console error -- "no server specified";
  elif [ -z "${value}" ]; then
    console error -- "require expects %s or %s" true false;
  else
    validate.boolean "require" "${value}" || return 1;
    local fetch=false;
    config set couch_httpd_auth require_valid_user "${value}";
    if ${flags[success]}; then
      console info -- "require valid user %s for %s" "${value}" "${server}";
    fi
  fi
}

auth.commands.ls() {
  local k v userurl;
  for k in ${!sessions[@]}
    do
      v="${sessions[$k]}";
      auth.userurl "${k}" "${v}";
      console print -- "${userurl}";
  done
}

# session
auth.commands.session() {
  local namespace="session.commands";
  local cmd="${1:-}"; shift;
  if ! method.exists? "${namespace}.${cmd}"; then
    console error -- "unknown session command %s" "${cmd}";
  else
    delegate "${namespace}" "${cmd}" "$@";
  fi
}

session.commands.login() {
  local server="${1:-${info[server]:-}}";
  local username="${2:-}";
  local repeat="${3:-false}";
  local password="${loginpass:-}";
  unset loginpass; # NOTE: prevent infinite login loop
  local aliasorserver="${server}";
  local userurl;
  alias.expand || return 1;
  if [ -z "${password}" ]; then
    if [ -z "${username}" ]; then
      auth.credentials;
    else
      auth.password;
    fi
  fi
  # NOTE: additional false option to prevent
  # NOTE: a login from being repeated on successful login
  couchdb.session.login \
    "${server}" "${username}" "${password}";
  if [ ${http[status]} -eq 200 ]; then
    defaults[login.attempts]=0;
    couchdb[cookie]="${files[cookie]}";
    sessions["${server}"]="${username}";
    auth.userurl "${server}" "${username}";
    console info -- "authenticated %s" "${userurl}";
    # NOTE: only change directory if no server is selected
    if [ -z "${info[server]:-}" ]; then
      rlx.commands.cd "${aliasorserver}" "" false;
    fi
    # NOTE: repeat last request on successful auth
    if [ "${repeat}" == true ]; then
      rlx.repeat;
    fi
    return 0;
  elif [ ${http[status]} -eq 401 ]; then
    defaults[login.attempts]=$((defaults[login.attempts]+=1));
    local maximum="${defaults[login.attempts.maximum]}";
    if [ "${defaults[login.attempts]}" -eq $((maximum)) ]; then
      defaults[login.attempts]=0;
      console error -- "too many failed logins";
      return 1;
    fi
    rlx.json.error;
    session.commands.login "${server}" "" true;
  else
    rlx.json.error;
  fi
}

session.commands.get() {
  local server="${1:-${info[server]:-}}";
  local print="${print:-true}";
  alias.expand || return 1;
  if [ -z "${server}" ]; then
    console error -- "no server specified or selected";
    return 1;
  else
    rlx.run couchdb.session "${server}";
    if ${flags[success]}; then
      if $print; then
        rlx.json.print;
      fi
      return 0;
    fi
  fi
}

session.commands.logout() {
  local server="${1:-${info[server]:-}}";
  local username="${sessions[server]:-}";
  local userurl;
  alias.expand || return 1;
  auth.clear;
  couchdb[cookie]="";
  rlx.run couchdb.session.logout "${server}";
  if ${flags[success]}; then
    auth.userurl "${server}" "${username}";
    console info -- "logout %s" "${userurl}";
    if [ "${server}" == "${info[server]:-}" ]; then
      cookie.clear;
      server.clear;
    fi
  fi
}

cookie.clear() {
  if [ -f "${files[cookie]}" ]; then
    if [[ "${server}" =~ ${regexp[domain]} ]]; then 
      local domain="${BASH_REMATCH[2]}";
      local lines=();
      local parts=();
      local first;
      while IFS= read -r line
        do
          parts=( $line );
          if [ ${#parts[@]} -gt 0 ]; then
            first="${parts[0]}";
            if [ "${first}" == "${domain}" ] \
              || [ "${first}" == "#HttpOnly_$domain" ]; then
              continue;
            else
              lines+=( "$line" );
            fi
          fi
      done < "${files[cookie]}";

      # write out the updated data
      local IFS=$'\n';
      echo "${lines[*]}" >| "${files[cookie]}";
      unset IFS;
    else
      console error -- "could not determine domain name from %s" "${server}";
    fi
  fi
}

auth.password() {
  local userurl;
  auth.userurl "${server}" "${username}";
  local prefix="[password]";
  console prompt --prefix="${prefix}" \
    "%s" "${userurl}";
  password() {
    password="${1:-}";
    rlx.prompt;
  }
  prompt line \
    --response=password \
    --silent \
    --id=password;
}

auth.credentials() {
  local prefix="[username]";
  console prompt --prefix="${prefix}" "%s" "${server}";
  username() {
    username="${1:-}";
    auth.password;
  }
  prompt line \
    --response=username \
    --hist-file="${files[history]}" \
    --id=username;
}

auth.clear() {
  unset -v sessions auth;
  # all logins server -> username
  declare -Ag sessions;
  # current authenticated session
  declare -Ag auth;
}

auth.userurl() {
  local server="${1:-${auth[server]:-}}";
  local username="${2:-${auth[username]:-}}";
  if [ -z "${username:-}" ]; then
    userurl="${server}";
  else
    userurl="${username}@${server}";
    if [[ "${server}" =~ ${regexp[cdscheme]} ]]; then
      userurl=$( printf "%s%s@%s" "${BASH_REMATCH[1]}" \
        "${username}" "${BASH_REMATCH[2]}" );
    fi
  fi
}

## USER MANAGEMENT

# internal shortcut entry point
user() {
  rlx.commands.user "$@";
}

# main
rlx.commands.user() {
  if [ $# -eq 0 ]; then
    console error -- "no user command specified";
  else
    local cmd="${1:-}";
    if [ "${cmd}" != ns ]; then
      # find out the database name used for users 
      config get couch_httpd_auth authentication_db false;
      if ! ${flags[success]}; then
        console error -- "could not fetch users database name";
        return 1;
      fi
      local userdb="${info[config]:-}";
      # check the database exists
      db head "${userdb}";
      if ! ${flags[success]}; then
        console error -- "users database %s does not exist" "${userdb}";
        return 1;
      fi
    fi
    local delimiter=":";
    local userns="${rlx_settings[userns]:-}";
    local usertype="${rlx_settings[usertype]:-user}";
    user.namespace;
    local user_namespace="user.commands";
    shift;
    if ! method.exists? "${user_namespace}.${cmd}"; then
      console error -- "unknown user command %s" "${cmd}";
    else
      delegate "${user_namespace}" "${cmd}" "$@";
    fi
  fi
}

user.commands.db() {
  printf "${userdb}\n";
}

user.commands.ns() {
  local ns="${userns%$delimiter}";
  if [ -n "${ns}" ]; then
    printf "${ns}\n";
  fi
}

user.commands.ls() {
  doc ls "${userdb}";
}

user.commands.get() {
  local username="${1:-}";
  local rev="${2:-}";
  local print="${print:-true}";
  if [ -z "${username}" ]; then
    console error -- "no username specified";
  else
    user.id;
    db context "${userdb}" doc get "${username}" "${rev}";
  fi
}

user.commands.head() {
  local username="${1:-}";
  local rev="${2:-}";
  if [ -z "${username}" ]; then
    console error -- "no username specified";
  else
    user.id;
    #echo "making user head request with $username"
    db context "${userdb}" doc head "${username}" "${rev}";
  fi
}

user.commands.add() {
  local username="${1:-}";
  local roles=( ${@:2} );
  local template="${rlx_settings[tpluser]:-user}";
  local password1 password2;
  if user.validate; then
    local name="${username}";
    user.id;
    if user.password password1; then
      tpl clean;
      tpl array roles ${roles[@]:-};
      user.show;
    fi
  fi
  user.clean;
}

user.commands.edit() {
  local username="${1:-}";
  if user.validate; then
    user.id;
    doc edit "${username}" "" "${userdb}";
  fi
  user.clean;
}

user.commands.rm() {
  local username="${1:-}";
  if user.validate; then
    user.id;
    doc rm "${username}" "${userdb}";
  fi
}

## PRIVATE

user.clean() {
  rm "${files[input]}" 2>/dev/null;
  rm "${files[document]}" 2>/dev/null;
  rm "${directories[backup]}/document.json" 2>/dev/null;
}

user.validate() {
  if [ -z "${username}" ]; then
    console error -- "no username specified";
  else
    return 0;
  fi
  return 1;
}

user.show() {
  tpl parse "${template}" false \
    "id=${username}" \
    "type=${usertype}" \
    "name=${name}" \
    "password=${password1}" \
    || return 1;
  saved() {
    echo "user.show saved with user db '${userdb}'";
    rlx.run couchdb.doc.save \
      "${info[server]}" "${userdb}" \
      "${files[document]}" "${username}";
    if ${flags[success]}; then
      rlx.json.print;
    fi
  }
  rlx.edit "${files[input]}";
}

user.password() {
  local user="${username:-}";
  local varname="$1";
  #echo "capturse user password...$varname";
  local prefix="[password]";
  if [ "${varname}" == password1 ]; then
    console prompt --prefix="${prefix}" "%s" "${user}";
  else
    console prompt --prefix="${prefix}" "%s (%s)" "${user}" "confirm";
  fi
  password() {
    local passwd="${1:-}";
    variable.set "${varname}" "${passwd}";
    if [ "${varname}" == password2 ]; then
      if [ "${password1:-}" != "${password2:-}" ]; then
        console error -- "passwords do not match";
        user.password password2;
      else
        rlx.prompt;
        return 0;
      fi
    else
      user.password password2;
    fi
  }
  prompt line \
    --response=password \
    --silent \
    --id=password;
}

user.id() {
  username="${userns}${username}";
}

# prepare namespace
user.namespace() {
  if [ "${userns}" == false ]; then
    userns="";
  fi
  if [ -n "${userns}" ]; then
    userns="${userns}${delimiter}";
  fi
}

## ADMIN

# main
rlx.commands.admin() {
  local section="admins";
  if [ $# -eq 0 ]; then
    console error -- "no admin command specified";
  else
    local namespace="admin.commands";
    local cmd="${1:-}"; shift;
    if ! method.exists? "${namespace}.${cmd}"; then
      console error -- "unknown admin command %s" "${cmd}";
    else
      delegate "${namespace}" "${cmd}" "$@";
    fi
  fi
}

admin.commands.add() {
  local username="${1:-}";
  local server="${2:-${info[server]:-}}";
  local print="${3:-false}";
  if admin.validate; then
    config get $section;
    if ${flags[success]}; then
      local password1 password2;
      if user.password password1; then
        config set $section "${username}" "${password1}";
        if ${flags[success]}; then
          console ok -- "added admin %s" "${username}";
        fi
      fi
    else
      console error -- "could not fetch admins configuration";
      return 1;
    fi
  fi
}

admin.commands.ls() {
  local server="${1:-${info[server]:-}}";
  # TODO: remove the ability to specify print as an option
  local print="${2:-${print:-true}}";
  if admin.validate false; then
    config get $section;
  fi
}

admin.commands.party() {
  local server="${1:-${info[server]:-}}";
  if admin.validate false; then
    accepted() {
      admin ls "${server}" false;
      if ${flags[success]}; then
        rlx.interpreter keys;
        local admins=( $( "$interpreter" < "${files[response]}" || false ) );
        if [ $? -gt 0 ]; then
          console error -- "error processing administrator list";
          rlx.debug "%s encountered an error with the document %s" \
            "${interpreter}" "${files[response]}";
          return 1;
        fi
        if [ ${#admins[@]} -eq 0 ]; then
          console info -- "no administrators found";
        else
          local administrator;
          for administrator in "${admins[@]}"
            do
              console info -- "administrator %s" \
                "${administrator}";
          done
          accepted() {
            local force=true;
            local user;
            for user in "${admins[@]}"
              do
                admin rm "${user}" "${server}";
                if ! ${flags[success]}; then
                  console error -- "could not delete administrator %s" \
                    "${user}";
                  return 1;
                fi
            done
            rlx.prompt;
          }
          console prompt --program \
            "delete %s administrator(s)? (y/n)" "${#admins[@]}";
          prompt confirm \
            --accepted=accepted \
            --rejected=rlx.rejected \
            --id=delete;
        fi
      else
        console error -- "failed to fetch administrator list";
      fi
      rlx.prompt;
    }
    console prompt --program \
      "admin party mode, are you %s sure? (y/n)" "really";
    prompt confirm \
      --accepted=accepted \
      --rejected=rlx.rejected \
      --id=delete;
  fi
}

admin.commands.rm() {
  local username="${1:-}";
  local server="${2:-${info[server]:-}}";
  if admin.validate; then
    local authuser="";
    local sameuser=false;
    if [ -n "${server}" ]; then
      authuser="${sessions[$server]:-}"
    fi
    if [ -n "${authuser}" ] && [ "${authuser}" == "${username}" ]; then
      console warn -- "delete authenticated admin %s" "${username}";
      sameuser=true;
    fi
    accepted() {
      config rm $section "${username}" >/dev/null;
      if ${flags[success]}; then
        console ok -- "deleted admin %s" "${username}";
      fi
      if $sameuser; then
        unset sessions[$server];
      fi
      rlx.prompt;
    }
    if [ "${force}" == true ]; then
      accepted;
    else
      console prompt --program \
        "delete admin %s? (y/n)" "${username}";
      prompt confirm \
        --accepted=accepted \
        --rejected=rlx.rejected \
        --id=delete;
    fi
  fi
}

admin.validate() {
  local user_required="${1:-true}";
  alias.expand || return 1;
  if $user_required && [ -z "${username}" ]; then
    console error -- "no admin username specified";
    return 1;
  elif [ -z "${server:-}" ]; then
    console error -- "no server selected";
    return 1;
  fi
  return 0;
}

## SECURITY

security() {
  rlx.commands.security "$@";
}

# main
rlx.commands.security() {

  if [ $# -eq 0 ]; then
    security.commands.get;
  else
    local namespace="security.commands";
    local cmd="${1:-}"; shift;
    if ! method.exists? "${namespace}.${cmd}"; then
      console error -- "unknown security command %s" "${cmd}";
    else
      delegate "${namespace}" "${cmd}" "$@";
    fi
  fi
}

security.commands.get() {
  local db="${1:-${info[database]:-}}";
  local server="${2:-${info[server]:-}}";
  local print="${3:-${print:-true}}";
  if security.validate; then
    rlx.run couchdb.security.get \
      "${server}" "${db}";
    if ${flags[success]}; then
      if [ "${print}" == true ]; then
        rlx.json.print;
        return 0;
      fi
    else
      console error -- "could not retrieve security information";
      return 1;
    fi
  else
    return 1;
  fi
}

security.commands.add() {
  local db="${1:-${info[database]:-}}";
  local server="${2:-${info[server]:-}}";
  local print=false;
  if security.validate; then
    security get "${db}" "${server}" || return 1;
    if ${flags[success]}; then
      local loaded=true;
      local json=$( < "${files[response]}" );
      if [ "${json}" != "${defaults[object]}" ]; then
        security edit "${db}" "${server}";
      else
        tpl clean;
        tpl parse security/add false || return 1;
        saved() {
          security.saved;
        }
        rlx.edit "${files[input]}";
      fi
    fi
  else
    return 1;
  fi
}

security.commands.edit() {
  local db="${1:-${info[database]:-}}";
  local server="${2:-${info[server]:-}}";
  local print=false;
  local loaded="${loaded:-false}";
  if security.validate; then
    if ! $loaded; then
      security get "${db}" "${server}" || return 1;
    fi
    if ${flags[success]}; then
      local json=$( < "${files[response]}" );
      # indent response document
      rlx.interpreter;
      "$interpreter" < "${files[response]}" >| "${files[input]}" \
        || return 1;
      saved() {
        security.saved;
      }
      rlx.edit "${files[input]}";
    fi
  else
    return 1;
  fi
}

security.commands.rm() {
  local db="${1:-${info[database]:-}}";
  local server="${2:-${info[server]:-}}";
  if security.validate; then
    accepted() {
      security.save "${defaults[object]}";
      rlx.prompt;
    }
    console prompt --program \
      "delete all administrators and members of %s? (y/n)" "${db}";
    prompt confirm \
      --accepted=accepted \
      --rejected=rlx.rejected \
      --id=delete;
  else
    return 1;
  fi
}

## PRIVATE

security.saved() {
  rlx.json.compact "${files[document]}" "${files[input]}";
  security.save;
}

security.save() {
  #local json="${1:-}";
  #if [ -z "${json}" ]; then
    #json=$( < "${files[input]}" );
  #fi
  rlx.run couchdb.security.set \
    "${server}" "${db}" "${files[input]}";
  rlx.file.cleanup "${files[input]}";
  if ${flags[success]}; then
    console info -- "saved security object for %s" "${db:-}";
  else
    console error -- "could not save security information";
    return 1;
  fi
}

security.validate() {
  if [ -z "${server:-}" ]; then
    console error -- "no server specified";
    return 1;
  elif [ -z "${db:-}" ]; then
    console error -- "no database specified";
    return 1;
  fi
  return 0;
}

## PASSWD

rlx.commands.passwd() {
  local section="admins";
  local server="${info[server]:-}";
  local username="${1:-}";
  local authuser="${defaults[username]}";
  local authroles=();
  local privileged=false;
  local user_exists=false;
  if passwd.validate; then
    local print=false;
    # first fetch session information
    session "${server}" || return 1;
    if ${flags[success]}; then
      rlx.interpreter authenticated;
      local parts=( $( "$interpreter" < "${files[response]}" || false ) );
      if [ $? -eq 0 ] && [ ${#parts[@]} -gt 0 ]; then
        authuser="${parts[0]}"; unset parts[0];
        if [ -z "${authuser}" ]; then
          console error -- "internal error retrieving current user";
        # set target user to currently authenticated user
        elif [ -z "${username}" ]; then
          username="${authuser}";
        fi
        local IFS=$'\n'; authroles=( "${parts[@]:-}" ); unset IFS;
        if array.contains? "_admin" "${authroles[@]:-}"; then
          privileged=true;
        fi
      fi
      if [ "${authuser}" == "${defaults[username]}" ]; then
        console error -- "authentication required for %s" "${server}";
        console error -- "use the %s command to authenticate" "login";
        return 1;
      fi
      local userid="${rlx_defaults[userns]}:${username}";
      rlx.interpreter authdb;
      local userdb=$( "${interpreter}" < "${files[response]}" || false );
      if [ $? -gt 0 ]; then
        console error -- "could not retrieve authentication database name";
        return 1;
      fi
      if ! $privileged && [ "${authuser}" != "${username}" ]; then
        console error -- "users are not allowed to modify other users";
        console error -- "run %s with no options to change your own password" \
          "passwd";
        return 1;
      fi
      rlx.debug "authentication database %s" "${userdb}";
      console info -- "authenticated user %s" "${authuser}";
      console info -- "changing password for %s" "${username}";
      if $privileged; then
        passwd.admin;
      else
        passwd.user;
      fi
    fi
  fi
}

## PRIVATE

# fetch user document
passwd.user.document() {
  couchdb.doc.get "${server}" "${userdb}" "${userid}";
  if [ "${http[status]}" == 200 ]; then
    user_exists=true;
    return 0;
  elif [ "${http[status]}" == 404  ]; then
    return 1;
  elif [ "${http[status]}" != 404 ]; then
    rlx.json.error;
    return 2;
  fi
}

passwd.admin() {
  local password1 password2;
  # administrator changing another users password
  if [ "${authuser}" != "${username}" ]; then
    admin ls;
    rlx.interpreter keys;
    local administrators=( $( "${interpreter}" < "${files[response]}" ) );
    if array.contains? "${username}" "${administrators[@]:-}"; then
      local msg="administrators may not change the password ";
      msg+="of another administrator";
      console error -- "${msg}";
      return 1;
    fi
    passwd.user.document;
    local usertest=$?;
    if [ ${usertest} -eq 0 ]; then
      user.password password1;
      passwd.user "${password1}" false;
    elif [ ${usertest} -eq 1 ]; then
      console error -- "user %s does not exist" "${username}";
      return 1;
    fi
  # administrator changing their own password
  else
    user.password password1;
    passwd.user.document;
    if $user_exists; then
      passwd.user "${password1}" false \
        && passwd.admin.update;
    else
      passwd.admin.update;
    fi
  fi
}

passwd.admin.update() {
  # don't fetch the updated config as that would
  # prompt for re-authentication with the new password
  local fetch=false;
  config set $section "${authuser}" "${password1}";
  if ${flags[success]}; then
    passwd.updated;
  fi
}

passwd.user() {
  local password1="${1:-}";
  local fetch="${2:-true}";
  local password2;
  if $fetch; then
    passwd.user.document;
  fi
  if [ -z "${password1}" ]; then
    user.password password1;
  fi
  console info -- "setting password for %s" "${userid}";
  # FIXME: prevent this document from being written to disc
  tpl clean;
  local userdoc=$( cat "${files[response]}" || false );
  tpl clean;
  tpl parse passwd false \
    "doc=${userdoc}" \
    "password=${password1}";
  #local userdoc=$( cat "${files[input]}" );
  tpl clean;
  #echo "final user doc $userdoc";
  couchdb.doc.save "${server}" "${userdb}" "${files[input]}" "${userid}";
  rlx.file.cleanup "${files[input]}";
  #rlx.commands.net;
  if [[ ! "${http[status]}" =~ ^2 ]]; then
    rlx.json.error;
    return 1;
  fi
  # if we are fetching then we have not come from
  # an administrator so print success
  if $fetch; then
    passwd.updated;
  fi
  return 0;
}

passwd.updated() {
  console ok -- "updated password for %s" "${username}";
  if [ "${authuser}" == "${username}" ]; then
    logout;
    local loginpass="${password1}";
    login "${server}" "${authuser}";
  fi
}

passwd.validate() {
  if [ -z "${server}" ]; then
    console error -- "no server selected";
    return 1;
  fi
  return 0;
}
