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

deploy_dir="${ARBITER_DOCKER_DIR:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)}"
compose_file="$deploy_dir/compose.yaml"
compose_env_file="$deploy_dir/docker.env"
requirements_file="$deploy_dir/requirements.txt"
compose_override_file="$deploy_dir/compose.override.yaml"

usage() {
  cat <<'EOF'
Usage: arbiter-docker COMMAND

Commands:
  sync-env      Run arbiter-server env bootstrap for the configured config
  edit-config   Edit the main config file with $ARBITER_EDITOR, $EDITOR, or vi
  edit-requirements
                Edit Python requirements installed inside the container
  edit-env      Edit the configured env file with $ARBITER_EDITOR, $EDITOR, or vi
  edit-docker   Edit docker.env with $ARBITER_EDITOR, $EDITOR, or vi
  up            Start or update the Docker Compose service
  restart       Recreate the Docker Compose service
  down          Stop and remove the Docker Compose service
  ps            Show Docker Compose service status
  logs          Follow Docker Compose logs
  info          Show deployment paths and Docker Compose version
  doctor        Check generated files, Docker Compose, and optional agent access
  install       Promote this prepared deployment directory to a Linux host

Doctor options:
  --preinstall       Check this directory before sudo install; skip Docker daemon checks
  --agent-user USER   Check that USER cannot read/write deployment state
  --agent-uid UID     Check that UID cannot read/write deployment state

Install options:
  --to DIR            Install target directory, default /opt/arbiter
  --user USER         Dedicated service config owner, default arbiter
  --group GROUP       Dedicated service config group, default USER
  --service NAME      systemd service name, default arbiter
  --no-start          Install and enable the service without starting it
  --dry-run           Print the install plan without changing the host

Environment:
  ARBITER_DOCKER_DIR   Deployment directory, default script directory
  ARBITER_EDITOR       Editor for edit-config/edit-env
EOF
}

require_file() {
  local path="$1"
  [[ -f "$path" ]] || {
    printf 'error: missing file: %s\n' "$path" >&2
    exit 1
  }
}

require_dir() {
  local path="$1"
  [[ -d "$path" ]] || {
    printf 'error: missing directory: %s\n' "$path" >&2
    exit 1
  }
}

validate_requirements_file() {
  local path="$1"

  awk '
    {
      line = $0
      sub(/^[[:space:]]+/, "", line)
      sub(/[[:space:]]+$/, "", line)
      if (line == "" || line ~ /^#/) {
        next
      }
      sub(/[[:space:]]+#.*$/, "", line)
      sub(/[[:space:]]+$/, "", line)
      if (line ~ /^\//) {
        next
      }
      if (line ~ /^[A-Za-z0-9][A-Za-z0-9_.-]*(\[[A-Za-z0-9_.-]+(,[A-Za-z0-9_.-]+)*\])?==[^<>=!~[:space:]#]+$/) {
        name = line
        sub(/==.*/, "", name)
        sub(/\[.*/, "", name)
        version = line
        sub(/^[^=]*==/, "", version)
        if ((name in pins) && pins[name] != version) {
          printf "%s:%d: conflicting package pins for %s: %s and %s\n", FILENAME, FNR, name, pins[name], version
          invalid = 1
        }
        pins[name] = version
        if (name == "arbiter-suite") {
          meta_all = 1
        }
        if (name == "arbiter-core" || name == "arbiter-smtp" || name == "arbiter-imap") {
          all_component = 1
        }
        next
      }
      printf "%s:%d: requirement must be an exact package pin (name==version) or an absolute container path: %s\n", FILENAME, FNR, $0
      invalid = 1
    }
    END {
      if (meta_all && all_component) {
        printf "%s: arbiter-suite meta package cannot be combined directly with arbiter-core, arbiter-smtp, or arbiter-imap pins; generate expanded real package pins with arbiter-server deploy docker docker.requirement=...\n", FILENAME
        invalid = 1
      }
      exit invalid
    }
  ' "$path"
}

edit_requirements() {
  local edited_requirements

  require_file "$requirements_file"
  edited_requirements="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-requirements.XXXXXX")"
  cp "$requirements_file" "$edited_requirements"

  if ! run_editor "$edited_requirements"; then
    rm -f "$edited_requirements"
    return 1
  fi

  if ! validate_requirements_file "$edited_requirements"; then
    printf 'error: requirements unchanged: %s\n' "$requirements_file" >&2
    rm -f "$edited_requirements"
    return 1
  fi

  cp "$edited_requirements" "$requirements_file"
  rm -f "$edited_requirements"
  printf 'updated requirements file: %s\n' "$requirements_file"
}

env_file_value() {
  local env_path="$1"
  local key="$2"
  [[ -f "$env_path" ]] || return 1

  awk -v key="$key" '
    $0 ~ "^[[:space:]]*#" { next }
    index($0, key "=") == 1 {
      print substr($0, length(key) + 2)
      found = 1
      exit
    }
    END {
      if (!found) {
        exit 1
      }
    }
  ' "$env_path"
}

compose_env_value() {
  local key="$1"
  local default="$2"

  env_file_value "$compose_env_file" "$key" || printf '%s\n' "$default"
}

deploy_path_from_compose_value() {
  local value="$1"

  if [[ "$value" = /* ]]; then
    printf '%s\n' "$value"
  else
    printf '%s/%s\n' "$deploy_dir" "${value#./}"
  fi
}

config_dir_path() {
  deploy_path_from_compose_value "$(compose_env_value ARBITER_CONFIG_DIR ./conf)"
}

config_name_value() {
  compose_env_value ARBITER_CONFIG_NAME arbiter-server
}

app_env_path() {
  deploy_path_from_compose_value "$(compose_env_value ARBITER_APP_ENV_FILE ./conf/.env)"
}

config_main_file() {
  printf '%s/%s.yaml\n' "$(config_dir_path)" "$(config_name_value)"
}

sync_env() {
  local active_config_dir
  local active_config_name

  active_config_dir="$(config_dir_path)"
  active_config_name="$(config_name_value)"
  require_dir "$active_config_dir"
  require_file "$(config_main_file)"
  arbiter-server --config-dir "$active_config_dir" --config-name "$active_config_name" env bootstrap
}

run_editor() {
  local path="$1"
  local -a editor=()

  if [[ -n "${ARBITER_EDITOR:-}" ]]; then
    read -r -a editor <<<"$ARBITER_EDITOR"
  elif [[ -n "${EDITOR:-}" ]]; then
    read -r -a editor <<<"$EDITOR"
  else
    editor=(vi)
  fi

  "${editor[@]}" "$path"
}

compose() {
  local active_config_dir

  require_file "$compose_file"
  active_config_dir="$(config_dir_path)"
  require_dir "$active_config_dir"
  require_file "$(config_main_file)"
  require_file "$(app_env_path)"
  require_file "$compose_env_file"
  require_file "$requirements_file"
  validate_requirements_file "$requirements_file"

  cd "$deploy_dir"
  if [[ -f "$compose_override_file" ]]; then
    docker compose --env-file "$compose_env_file" -f "$compose_file" -f "$compose_override_file" "$@"
  else
    docker compose --env-file "$compose_env_file" -f "$compose_file" "$@"
  fi
}

info() {
  printf 'deploy dir: %s\n' "$deploy_dir"
  printf 'compose file: %s\n' "$compose_file"
  if [[ -f "$compose_override_file" ]]; then
    printf 'compose override file: %s\n' "$compose_override_file"
  fi
  printf 'config dir: %s\n' "$(config_dir_path)"
  printf 'config name: %s\n' "$(config_name_value)"
  printf 'app env file: %s\n' "$(app_env_path)"
  printf 'docker env file: %s\n' "$compose_env_file"
  printf 'requirements file: %s\n' "$requirements_file"
  docker compose version || true
}

doctor_status=0
doctor_agent_uid=""
doctor_agent_user=""
doctor_agent_groups=""
doctor_agent_group_names=""
doctor_preinstall=0

color_enabled() {
  [[ "${ARBITER_COLOR:-}" == always ]] && return 0
  [[ "${ARBITER_COLOR:-}" == never ]] && return 1
  [[ -t 1 ]]
}

doctor_prefix() {
  local color="$1"
  local label="$2"

  if color_enabled; then
    printf '\033[%sm%s\033[0m' "$color" "$label"
  else
    printf '%s' "$label"
  fi
}

doctor_line() {
  local color="$1"
  local label="$2"
  local message="$3"

  doctor_prefix "$color" "$label"
  printf ': %s\n' "$message"
}

doctor_ok() {
  doctor_line 32 ok "$1"
}

doctor_warn() {
  doctor_line 33 warn "$1"
}

doctor_fail() {
  doctor_line 31 fail "$1"
  doctor_status=1
}

doctor_check_file() {
  local path="$1"
  local description="$2"

  if [[ -f "$path" ]]; then
    doctor_ok "$description exists: $path"
  else
    doctor_fail "$description is missing: $path"
  fi
}

path_is_under_deploy_dir() {
  local path="$1"
  local real_deploy_dir
  local real_path

  real_deploy_dir="$(readlink -f "$deploy_dir")"
  real_path="$(readlink -f "$path")"
  [[ "$real_path" == "$real_deploy_dir" || "$real_path" == "$real_deploy_dir/"* ]]
}

doctor_check_path_under_deploy_dir() {
  local path="$1"
  local description="$2"

  [[ -e "$path" ]] || return
  if path_is_under_deploy_dir "$path"; then
    doctor_ok "$description is inside deployment directory"
  else
    doctor_fail "$description is outside deployment directory: $path"
  fi
}

doctor_check_dir() {
  local path="$1"
  local description="$2"

  if [[ -d "$path" ]]; then
    doctor_ok "$description exists: $path"
  else
    doctor_fail "$description is missing: $path"
  fi
}

doctor_check_env_file() {
  local path="$1"
  local description="$2"

  if [[ ! -f "$path" ]]; then
    return
  fi

  if awk '
    /^[[:space:]]*($|#)/ { next }
    /^[A-Za-z_][A-Za-z0-9_]*=/ { next }
    {
      printf "%s:%d: invalid env assignment: %s\n", FILENAME, FNR, $0
      invalid = 1
    }
    END { exit invalid }
  ' "$path"; then
    doctor_ok "$description has valid KEY=VALUE lines"
  else
    doctor_fail "$description contains invalid env lines"
  fi
}

doctor_check_requirements_file() {
  local path="$1"

  if [[ ! -f "$path" ]]; then
    return
  fi

  if validate_requirements_file "$path"; then
    doctor_ok "requirements file uses exact pins or absolute container paths"
  else
    doctor_fail "requirements file contains unpinned package requirements"
  fi
}

doctor_group_contains() {
  local gid="$1"
  local group

  for group in $doctor_agent_groups; do
    [[ "$group" == "$gid" ]] && return 0
  done
  return 1
}

doctor_agent_perm_digit() {
  local path="$1"
  local owner_uid
  local owner_gid
  local mode
  local perms

  read -r owner_uid owner_gid mode < <(stat -c '%u %g %a' "$path")
  perms="${mode: -3}"

  if [[ "$doctor_agent_uid" == "$owner_uid" ]]; then
    printf '%s\n' "${perms:0:1}"
  elif doctor_group_contains "$owner_gid"; then
    printf '%s\n' "${perms:1:1}"
  else
    printf '%s\n' "${perms:2:1}"
  fi
}

doctor_agent_has_perm() {
  local path="$1"
  local bit="$2"
  local digit

  [[ -e "$path" ]] || return 1
  digit="$(doctor_agent_perm_digit "$path")"
  (( (10#$digit & bit) != 0 ))
}

doctor_check_agent_cannot_read() {
  local path="$1"
  local description="$2"

  [[ -e "$path" ]] || return
  if doctor_agent_has_perm "$path" 4; then
    doctor_fail "$description is readable by agent identity: $path"
  else
    doctor_ok "$description is not readable by agent identity"
  fi
}

doctor_check_agent_cannot_write_file() {
  local path="$1"
  local description="$2"

  [[ -e "$path" ]] || return
  if doctor_agent_has_perm "$path" 2; then
    doctor_fail "$description is writable by agent identity: $path"
  else
    doctor_ok "$description is not writable by agent identity"
  fi
}

doctor_check_agent_cannot_write_dir() {
  local path="$1"
  local description="$2"

  [[ -d "$path" ]] || return
  if doctor_agent_has_perm "$path" 2 && doctor_agent_has_perm "$path" 1; then
    doctor_fail "$description is writable by agent identity: $path"
  else
    doctor_ok "$description is not writable by agent identity"
  fi
}

doctor_dir_has_sticky_bit() {
  local path="$1"
  local mode
  local special

  mode="$(stat -c '%a' "$path")"
  special=$((10#$mode / 1000 % 10))
  (( (special & 1) != 0 ))
}

doctor_check_agent_cannot_replace_deploy_dir() {
  local deploy_parent

  deploy_parent="$(dirname "$deploy_dir")"
  [[ -d "$deploy_parent" ]] || return

  if ! doctor_agent_has_perm "$deploy_parent" 2 || ! doctor_agent_has_perm "$deploy_parent" 1; then
    doctor_ok "deployment parent directory does not allow agent replacement"
    return
  fi

  if doctor_dir_has_sticky_bit "$deploy_parent"; then
    doctor_warn "deployment parent directory is writable but sticky: $deploy_parent"
    return
  fi

  doctor_fail "deployment parent directory allows agent replacement: $deploy_parent"
}

doctor_check_docker_socket() {
  local socket="/var/run/docker.sock"

  if [[ ! -e "$socket" ]]; then
    doctor_warn "Docker socket not found at $socket"
    return
  fi

  if doctor_agent_has_perm "$socket" 2; then
    doctor_fail "Docker socket is writable by agent identity: $socket"
  else
    doctor_ok "Docker socket is not writable by agent identity"
  fi
}

doctor_check_preinstall_ready() {
  local active_config_dir
  local active_app_env

  active_config_dir="$(config_dir_path)"
  active_app_env="$(app_env_path)"

  doctor_check_path_under_deploy_dir "$active_config_dir" "config directory"
  doctor_check_path_under_deploy_dir "$active_app_env" "app env file"

  if [[ -f "$requirements_file" ]] && grep -Eq '^[[:space:]]*/source/arbiter(/|$)' "$requirements_file"; then
    doctor_fail "preinstall cannot promote local source requirements: $requirements_file"
    printf '      use pinned packages (name==version) or /wheels/*.whl entries before install\n'
  fi

  if [[ -f "$compose_override_file" ]] && grep -q '/source/arbiter' "$compose_override_file"; then
    doctor_fail "preinstall cannot promote a local source mount: $compose_override_file"
    printf '      remove the /source/arbiter mount after switching requirements\n'
  fi

  if [[ "$doctor_status" -eq 0 ]]; then
    doctor_ok "preinstall checks passed"
  fi
}

doctor_check_docker_network_subnet() {
  local configured_network
  local configured_subnet
  local network_line
  local network_name
  local network_subnet

  configured_network="$(compose_env_value ARBITER_DOCKER_NETWORK_NAME arbiter)"
  configured_subnet="$(compose_env_value ARBITER_DOCKER_SUBNET 172.31.250.0/24)"
  [[ -n "$configured_subnet" ]] || return

  if ! docker network ls >/dev/null 2>&1; then
    return
  fi

  while read -r network_line; do
    [[ -n "$network_line" ]] || continue
    network_name="${network_line%% *}"
    network_subnet="${network_line#* }"
    [[ "$network_subnet" != "$network_line" ]] || continue
    [[ -n "$network_subnet" ]] || continue
    if [[ "$network_subnet" == "$configured_subnet" && "$network_name" != "$configured_network" ]]; then
      doctor_fail "Docker subnet $configured_subnet already belongs to network $network_name"
      return
    fi
  done < <(docker network inspect $(docker network ls -q) --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}} {{end}}' 2>/dev/null)

  doctor_ok "Docker subnet is not already used by another network"
}

doctor_resolve_agent() {
  if [[ -n "$doctor_agent_user" ]]; then
    if ! doctor_agent_uid="$(id -u "$doctor_agent_user" 2>/dev/null)"; then
      doctor_fail "agent user does not exist: $doctor_agent_user"
      return 1
    fi
    doctor_agent_groups="$(id -G "$doctor_agent_user")"
    doctor_agent_group_names="$(id -nG "$doctor_agent_user")"
    return 0
  fi

  if [[ -n "$doctor_agent_uid" ]]; then
    doctor_agent_user="$(getent passwd "$doctor_agent_uid" | cut -d: -f1 || true)"
    if [[ -z "$doctor_agent_user" ]]; then
      doctor_fail "agent uid does not resolve to a local user: $doctor_agent_uid"
      return 1
    fi
    doctor_agent_groups="$(id -G "$doctor_agent_user")"
    doctor_agent_group_names="$(id -nG "$doctor_agent_user")"
    return 0
  fi

  return 1
}

doctor_check_agent_access() {
  local active_config_dir
  local active_config_file
  local active_app_env

  if ! doctor_resolve_agent; then
    doctor_warn "skipping agent permission checks; pass --agent-user USER or --agent-uid UID"
    return
  fi

  doctor_ok "checking agent identity: ${doctor_agent_user:-uid $doctor_agent_uid} (uid $doctor_agent_uid)"

  if [[ " $doctor_agent_group_names " == *" docker "* ]]; then
    doctor_fail "agent identity is in the docker group"
  else
    doctor_ok "agent identity is not in the docker group"
  fi

  active_config_dir="$(config_dir_path)"
  active_config_file="$(config_main_file)"
  active_app_env="$(app_env_path)"
  doctor_check_agent_cannot_replace_deploy_dir
  doctor_check_agent_cannot_write_dir "$deploy_dir" "deployment directory"
  doctor_check_agent_cannot_write_file "$compose_file" "compose file"
  doctor_check_agent_cannot_write_dir "$active_config_dir" "config directory"
  doctor_check_agent_cannot_write_file "$active_config_file" "main config file"
  doctor_check_agent_cannot_write_file "$compose_env_file" "docker env file"
  doctor_check_agent_cannot_write_file "$active_app_env" "app env file"
  doctor_check_agent_cannot_write_file "$requirements_file" "requirements file"
  doctor_check_agent_cannot_write_file "$deploy_dir/arbiter-docker" "helper script"
  doctor_check_agent_cannot_read "$active_app_env" "app env file"
  doctor_check_docker_socket
  doctor_warn "permission checks do not inspect ACLs, sudo rules, or other direct service paths"
}

doctor() {
  local active_config_dir
  local active_config_file
  local active_app_env

  doctor_status=0
  doctor_agent_uid=""
  doctor_agent_user=""
  doctor_agent_groups=""
  doctor_agent_group_names=""
  doctor_preinstall=0

  while (($#)); do
    case "$1" in
      --preinstall)
        doctor_preinstall=1
        shift
        ;;
      --agent-user)
        [[ $# -ge 2 ]] || {
          printf 'error: --agent-user requires a value\n' >&2
          exit 2
        }
        doctor_agent_user="$2"
        shift 2
        ;;
      --agent-uid)
        [[ $# -ge 2 ]] || {
          printf 'error: --agent-uid requires a value\n' >&2
          exit 2
        }
        doctor_agent_uid="$2"
        shift 2
        ;;
      -h | --help)
        usage
        exit 0
        ;;
      *)
        printf 'error: unknown doctor option: %s\n\n' "$1" >&2
        usage >&2
        exit 2
        ;;
    esac
  done

  if [[ -n "$doctor_agent_user" && -n "$doctor_agent_uid" ]]; then
    printf 'error: use either --agent-user or --agent-uid, not both\n' >&2
    exit 2
  fi

  active_config_dir="$(config_dir_path)"
  active_config_file="$(config_main_file)"
  active_app_env="$(app_env_path)"
  doctor_check_dir "$deploy_dir" "deployment directory"
  doctor_check_file "$compose_file" "compose file"
  doctor_check_dir "$active_config_dir" "config directory"
  doctor_check_file "$active_config_file" "main config file"
  doctor_check_file "$active_app_env" "app env file"
  doctor_check_file "$compose_env_file" "docker env file"
  doctor_check_file "$requirements_file" "requirements file"
  doctor_check_file "$deploy_dir/arbiter-docker" "helper script"
  doctor_check_env_file "$active_app_env" "app env file"
  doctor_check_env_file "$compose_env_file" "docker env file"
  doctor_check_requirements_file "$requirements_file"

  if [[ "$doctor_preinstall" -eq 1 ]]; then
    doctor_check_preinstall_ready
    return "$doctor_status"
  fi

  if docker compose version >/dev/null 2>&1; then
    doctor_ok "$(docker compose version)"
  else
    doctor_fail "Docker Compose is not available"
  fi
  doctor_check_docker_network_subnet

  doctor_check_agent_access
  return "$doctor_status"
}

shell_quote() {
  printf '%q' "$1"
}

print_command() {
  local arg

  printf 'would run:'
  for arg in "$@"; do
    printf ' '
    shell_quote "$arg"
  done
  printf '\n'
}

run_install_command() {
  if [[ "$install_dry_run" -eq 1 ]]; then
    print_command "$@"
  else
    "$@"
  fi
}

install_require_root() {
  if [[ "$(id -u)" != 0 ]]; then
    printf 'error: install requires root; rerun with sudo\n' >&2
    exit 1
  fi
}

install_relative_path() {
  local path="$1"
  local real_deploy_dir
  local real_path

  real_deploy_dir="$(readlink -f "$deploy_dir")"
  real_path="$(readlink -f "$path")"
  printf '%s\n' "${real_path#"$real_deploy_dir"/}"
}

install_ensure_identity() {
  if [[ "$install_dry_run" -eq 1 ]]; then
    printf 'would create system group if missing: %s\n' "$install_group"
    printf 'would create system user if missing: %s\n' "$install_user"
    return
  fi

  if ! getent group "$install_group" >/dev/null; then
    groupadd --system "$install_group"
  fi

  if ! id -u "$install_user" >/dev/null 2>&1; then
    useradd \
      --system \
      --gid "$install_group" \
      --home-dir "$install_target_dir" \
      --shell /usr/sbin/nologin \
      --comment "Arbiter service user" \
      "$install_user"
  fi
}

install_copy_deployment() {
  local source_dir
  local target_dir

  source_dir="$(readlink -f "$deploy_dir")"
  target_dir="$install_target_dir"

  if [[ "$install_dry_run" -eq 1 ]]; then
    printf 'would copy deployment: %s -> %s\n' "$source_dir" "$target_dir"
    return
  fi

  mkdir -p "$target_dir"
  if [[ "$(readlink -f "$target_dir")" != "$source_dir" ]]; then
    cp -a "$source_dir/." "$target_dir/"
  fi
}

install_apply_permissions() {
  local app_env_rel
  local app_env_target

  if [[ "$install_dry_run" -eq 1 ]]; then
    printf 'would chown/chmod deployment for %s:%s: %s\n' \
      "$install_user" "$install_group" "$install_target_dir"
    return
  fi

  chown -R "$install_user:$install_group" "$install_target_dir"
  find "$install_target_dir" -type d -exec chmod 0750 {} +
  find "$install_target_dir" -type f -exec chmod 0640 {} +
  chmod 0750 "$install_target_dir/arbiter-docker"

  app_env_rel="$(install_relative_path "$(app_env_path)")"
  app_env_target="$install_target_dir/$app_env_rel"
  if [[ -f "$app_env_target" ]]; then
    chmod 0600 "$app_env_target"
  fi
}

install_write_systemd_unit() {
  local unit_file
  local docker_bin
  local compose_files

  unit_file="/etc/systemd/system/${install_service}.service"

  if [[ "$install_dry_run" -eq 1 ]]; then
    printf 'would write systemd unit: %s\n' "$unit_file"
    return
  fi

  docker_bin="$(command -v docker || true)"
  if [[ -z "$docker_bin" ]]; then
    printf 'error: docker command not found\n' >&2
    exit 1
  fi
  if ! command -v systemctl >/dev/null; then
    printf 'error: systemctl command not found\n' >&2
    exit 1
  fi
  compose_files="-f $install_target_dir/compose.yaml"
  if [[ -f "$compose_override_file" || -f "$install_target_dir/compose.override.yaml" ]]; then
    compose_files="$compose_files -f $install_target_dir/compose.override.yaml"
  fi

  cat >"$unit_file" <<EOF
[Unit]
Description=Arbiter Docker service
Requires=docker.service
After=docker.service

[Service]
Type=simple
WorkingDirectory=$install_target_dir
ExecStart=$docker_bin compose --env-file $install_target_dir/docker.env $compose_files up
ExecStop=$docker_bin compose --env-file $install_target_dir/docker.env $compose_files down
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF
  chmod 0644 "$unit_file"
}

install_deployment() {
  install_target_dir="/opt/arbiter"
  install_user="arbiter"
  install_group=""
  install_service="arbiter"
  install_start=1
  install_dry_run=0

  while (($#)); do
    case "$1" in
      --to)
        [[ $# -ge 2 ]] || {
          printf 'error: --to requires a value\n' >&2
          exit 2
        }
        install_target_dir="$2"
        shift 2
        ;;
      --user)
        [[ $# -ge 2 ]] || {
          printf 'error: --user requires a value\n' >&2
          exit 2
        }
        install_user="$2"
        shift 2
        ;;
      --group)
        [[ $# -ge 2 ]] || {
          printf 'error: --group requires a value\n' >&2
          exit 2
        }
        install_group="$2"
        shift 2
        ;;
      --service)
        [[ $# -ge 2 ]] || {
          printf 'error: --service requires a value\n' >&2
          exit 2
        }
        install_service="$2"
        shift 2
        ;;
      --no-start)
        install_start=0
        shift
        ;;
      --start)
        install_start=1
        shift
        ;;
      --dry-run)
        install_dry_run=1
        shift
        ;;
      -h | --help)
        usage
        exit 0
        ;;
      *)
        printf 'error: unknown install option: %s\n\n' "$1" >&2
        usage >&2
        exit 2
        ;;
    esac
  done

  if [[ "$install_target_dir" != /* ]]; then
    printf 'error: --to must be an absolute path: %s\n' "$install_target_dir" >&2
    exit 2
  fi
  if [[ "$install_target_dir" =~ [[:space:]] ]]; then
    printf 'error: --to must not contain whitespace: %s\n' "$install_target_dir" >&2
    exit 2
  fi
  if [[ ! "$install_service" =~ ^[A-Za-z0-9_.@-]+$ ]]; then
    printf 'error: --service contains unsupported characters: %s\n' "$install_service" >&2
    exit 2
  fi
  if [[ ! "$install_user" =~ ^[A-Za-z_][A-Za-z0-9_.-]*[$]?$ ]]; then
    printf 'error: --user contains unsupported characters: %s\n' "$install_user" >&2
    exit 2
  fi
  if [[ -z "$install_group" ]]; then
    install_group="$install_user"
  fi
  if [[ ! "$install_group" =~ ^[A-Za-z_][A-Za-z0-9_.-]*[$]?$ ]]; then
    printf 'error: --group contains unsupported characters: %s\n' "$install_group" >&2
    exit 2
  fi

  doctor --preinstall

  if [[ "$install_dry_run" -ne 1 ]]; then
    install_require_root
  fi

  install_ensure_identity
  install_copy_deployment
  install_apply_permissions
  install_write_systemd_unit
  run_install_command systemctl daemon-reload
  run_install_command systemctl enable "${install_service}.service"
  if [[ "$install_start" -eq 1 ]]; then
    run_install_command systemctl restart "${install_service}.service"
  else
    printf 'installed systemd service without starting it: %s.service\n' \
      "$install_service"
  fi
}

command="${1:-}"
if (($#)); then
  shift
fi

case "$command" in
  sync-env)
    sync_env
    ;;
  edit-config)
    require_file "$(config_main_file)"
    run_editor "$(config_main_file)"
    ;;
  edit-requirements)
    edit_requirements
    ;;
  edit-env)
    require_file "$(app_env_path)"
    run_editor "$(app_env_path)"
    ;;
  edit-docker)
    require_file "$compose_env_file"
    run_editor "$compose_env_file"
    ;;
  up)
    compose up -d
    ;;
  restart)
    compose up -d --force-recreate
    ;;
  down)
    compose down
    ;;
  ps)
    compose ps
    ;;
  logs)
    compose logs -f
    ;;
  info)
    info
    ;;
  doctor)
    doctor "$@"
    ;;
  install)
    install_deployment "$@"
    ;;
  "" | -h | --help | help)
    usage
    ;;
  *)
    printf 'error: unknown command: %s\n\n' "$command" >&2
    usage >&2
    exit 2
    ;;
esac
