#!/bin/sh
set -eu

INSTALL_DIR=${CONSOLEHUB_INSTALL_DIR:-/opt/consolehub}
ASSET_BASE_URL=${CONSOLEHUB_ASSET_BASE_URL:-https://consolehub.jp/install}
NON_INTERACTIVE=${CONSOLEHUB_NON_INTERACTIVE:-0}
TESTING=${CONSOLEHUB_TESTING:-0}
MINIMUM_COMPOSE_VERSION=2.24.0
MARKER_FILE=.consolehub-install
TEMP_DIR=

fail() {
  printf 'Console Hub installer: %s\n' "$*" >&2
  exit 1
}

cleanup() {
  if [ -n "${TEMP_DIR}" ] && [ -d "${TEMP_DIR}" ]; then
    rm -rf -- "${TEMP_DIR}"
  fi
}
trap cleanup EXIT HUP INT TERM

require_command() {
  if [ "${TESTING}" = 1 ] && [ "${CONSOLEHUB_TEST_MISSING_COMMAND:-}" = "$1" ]; then
    fail "required command not found: $1"
  fi
  command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
}

version_at_least() {
  first=$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n 1)
  [ "${first}" = "$1" ]
}

validate_install_dir() {
  case "${INSTALL_DIR}" in
    /*) ;;
    *) fail 'CONSOLEHUB_INSTALL_DIR must be an absolute path' ;;
  esac

  case "${INSTALL_DIR}" in
    /|/home|/root|/usr|/etc|/var|/opt|/tmp)
      fail "refusing unsafe installation directory: ${INSTALL_DIR}"
      ;;
  esac
}

validate_origin() {
  case "${ALLOWED_ORIGINS}" in
    http://*) origin_scheme=http; origin_tail=${ALLOWED_ORIGINS#http://} ;;
    https://*) origin_scheme=https; origin_tail=${ALLOWED_ORIGINS#https://} ;;
    *) fail 'ALLOWED_ORIGINS must be an exact HTTP or HTTPS origin' ;;
  esac
  case "${origin_tail}" in
    ''|*/*|*' '*|*','*|*'#'*|*'?'*|*'@'*|*'\\'*|*'*'*) fail 'ALLOWED_ORIGINS must be exact without credentials, wildcard, path, query, fragment, space, or mixed origins' ;;
    \[*|*\]*) fail 'IPv6 origins are not supported by the quick installer; use the manual installation path' ;;
  esac

  case "${origin_tail}" in
    *:*)
      origin_host=${origin_tail%:*}
      origin_port=${origin_tail##*:}
      case "${origin_host}" in *:*) fail 'IPv6 origins must use bracket notation' ;; esac
      validate_port_number "${origin_port}" ALLOWED_ORIGINS
      ;;
    *) origin_host=${origin_tail} ;;
  esac

  case "${origin_host:-${origin_tail}}" in
    ''|.*|*.|-*|*-|*[!A-Za-z0-9.-]*) fail 'ALLOWED_ORIGINS contains an invalid host' ;;
  esac

  if [ "${origin_scheme}" = http ]; then
    [ "${origin_host:-${origin_tail}}" = localhost ] || valid_private_ipv4 "${origin_host:-${origin_tail}}" || fail 'HTTP origin must use localhost or one exact private IPv4 address'
  fi
}

validate_port_number() {
  port_value=$1
  port_label=$2
  case "${port_value}" in
    ''|*[!0-9]*) fail "${port_label} contains an invalid port" ;;
  esac
  [ "${port_value}" -ge 1 ] && [ "${port_value}" -le 65535 ] || fail "${port_label} contains an invalid port"
}

valid_ipv4() {
  ipv4_value=$1
  case "${ipv4_value}" in ''|.*|*.|*..*|*[!0-9.]*) return 1 ;; esac
  previous_ifs=${IFS}
  IFS=.
  set -- ${ipv4_value}
  IFS=${previous_ifs}
  [ "$#" -eq 4 ] || return 1
  for octet in "$@"; do
    case "${octet}" in ''|*[!0-9]*) return 1 ;; esac
    case "${octet}" in 0[0-9]*) return 1 ;; esac
    [ "${octet}" -le 255 ] || return 1
  done
}

valid_private_ipv4() {
  valid_ipv4 "$1" || return 1
  previous_ifs=${IFS}
  IFS=.
  set -- $1
  IFS=${previous_ifs}
  case "$1.$2" in
    10.*) return 0 ;;
    172.*) [ "$2" -ge 16 ] && [ "$2" -le 31 ] ; return ;;
    192.168) return 0 ;;
    100.*) [ "$2" -ge 64 ] && [ "$2" -le 127 ] ; return ;;
    *) return 1 ;;
  esac
}

list_private_ipv4() {
  command -v hostname >/dev/null 2>&1 || return 1
  for candidate in $(hostname -I 2>/dev/null); do
    if valid_private_ipv4 "${candidate}"; then
      printf '%s\n' "${candidate}"
    fi
  done
}

host_has_ipv4() {
  command -v hostname >/dev/null 2>&1 || return 1
  for candidate in $(hostname -I 2>/dev/null); do
    if [ "${candidate}" = "$1" ]; then return 0; fi
  done
  return 1
}

validate_trusted_proxy() {
  proxy_value=${TRUSTED_PROXY}
  [ -n "${proxy_value}" ] || fail 'TRUSTED_PROXY is required'
  case "${proxy_value}" in
    */*) proxy_address=${proxy_value%/*}; proxy_prefix=${proxy_value##*/} ;;
    *) proxy_address=${proxy_value}; proxy_prefix= ;;
  esac

  valid_ipv4 "${proxy_address}" || fail 'TRUSTED_PROXY must be an explicit IPv4 address or CIDR'
  proxy_max_prefix=32

  if [ -n "${proxy_prefix}" ]; then
    case "${proxy_prefix}" in ''|*[!0-9]*) fail 'TRUSTED_PROXY contains an invalid CIDR prefix' ;; esac
    [ "${proxy_prefix}" -le "${proxy_max_prefix}" ] || fail 'TRUSTED_PROXY contains an invalid CIDR prefix'
  fi
}

validate_bind_address() {
  valid_ipv4 "${APP_BIND_ADDRESS}" || fail 'APP_BIND_ADDRESS must be an explicit IPv4 address'
}

validate_port() {
  case "${APP_PORT}" in
    ''|*[!0-9]*) fail 'APP_PORT must be a number from 1 to 65535' ;;
  esac
  [ "${APP_PORT}" -ge 1 ] && [ "${APP_PORT}" -le 65535 ] || fail 'APP_PORT must be a number from 1 to 65535'
}

prompt_configuration() {
  ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-}
  TRUSTED_PROXY=${TRUSTED_PROXY:-}
  APP_BIND_ADDRESS=${APP_BIND_ADDRESS:-}
  APP_PORT=${APP_PORT:-3000}
  TZ=${TZ:-Asia/Tokyo}

  if [ "${NON_INTERACTIVE}" != 1 ]; then
    printf 'Access directly by LAN IP without a reverse proxy? [Y/n]: '
    IFS= read -r direct_answer
    case "${direct_answer}" in n|N|no|NO) direct_install=0 ;; *) direct_install=1 ;; esac
    if [ "${direct_install}" = 1 ] && [ -z "${APP_BIND_ADDRESS}" ]; then
      detected_addresses=$(list_private_ipv4 || true)
      if [ -n "${detected_addresses}" ]; then
        printf 'Detected private IPv4 addresses:\n'
        detected_count=0
        for candidate in ${detected_addresses}; do
          printf '  %s\n' "${candidate}"
          detected_count=$((detected_count + 1))
          detected_address=${candidate}
        done
        if [ "${detected_count}" -eq 1 ]; then APP_BIND_ADDRESS=${detected_address}; fi
      fi
    fi
    printf 'Application bind address%s: ' "${APP_BIND_ADDRESS:+ [${APP_BIND_ADDRESS}]}"
    IFS= read -r entered_bind
    APP_BIND_ADDRESS=${entered_bind:-${APP_BIND_ADDRESS}}
    printf 'Application port [%s]: ' "${APP_PORT}"
    IFS= read -r entered_port
    APP_PORT=${entered_port:-${APP_PORT}}
    if [ "${direct_install}" = 1 ]; then
      [ -n "${APP_BIND_ADDRESS}" ] || fail 'a private server IPv4 or 127.0.0.1 is required for direct HTTP access'
      if [ "${APP_BIND_ADDRESS}" = 127.0.0.1 ]; then
        if [ "${APP_PORT}" = 80 ]; then ALLOWED_ORIGINS=http://localhost; else ALLOWED_ORIGINS="http://localhost:${APP_PORT}"; fi
      else
        ALLOWED_ORIGINS="http://${APP_BIND_ADDRESS}:${APP_PORT}"
      fi
      TRUSTED_PROXY=
    else
      if [ -z "${ALLOWED_ORIGINS}" ]; then
        printf 'Browser-visible HTTPS origin: '
        IFS= read -r ALLOWED_ORIGINS
      fi
      if [ -z "${TRUSTED_PROXY}" ]; then
        printf 'Trusted reverse-proxy IP or CIDR: '
        IFS= read -r TRUSTED_PROXY
      fi
    fi
  fi

  validate_bind_address
  validate_port
  validate_origin
  if [ "${origin_scheme}" = http ]; then
    [ -z "${TRUSTED_PROXY}" ] || fail 'TRUSTED_PROXY must be empty for direct HTTP access'
    if [ "${origin_host:-${origin_tail}}" = localhost ]; then
      [ "${APP_BIND_ADDRESS}" = 127.0.0.1 ] || fail 'localhost HTTP origin requires APP_BIND_ADDRESS=127.0.0.1'
      if [ "${APP_PORT}" = 80 ]; then expected_origin=http://localhost; else expected_origin="http://localhost:${APP_PORT}"; fi
      [ "${ALLOWED_ORIGINS}" = "${expected_origin}" ] || fail 'localhost HTTP origin port must match APP_PORT'
    else
      expected_origin="http://${APP_BIND_ADDRESS}:${APP_PORT}"
      [ "${ALLOWED_ORIGINS}" = "${expected_origin}" ] || fail 'HTTP origin must exactly match APP_BIND_ADDRESS and APP_PORT'
      host_has_ipv4 "${APP_BIND_ADDRESS}" || fail 'APP_BIND_ADDRESS is not assigned to this installation host'
    fi
  else
    validate_trusted_proxy
  fi
}

validate_platform() {
  [ "$(uname -s)" = Linux ] || fail 'only Linux installation hosts are supported'
  case "$(uname -m)" in
    x86_64|amd64|aarch64|arm64) ;;
    *) fail "unsupported architecture: $(uname -m)" ;;
  esac

  require_command docker
  require_command curl
  require_command sha256sum
  require_command sort

  docker compose version >/dev/null 2>&1 || fail 'Docker Compose v2 is required'
  compose_version=$(docker compose version --short 2>/dev/null | sed 's/^v//; s/+.*$//')
  version_at_least "${MINIMUM_COMPOSE_VERSION}" "${compose_version}" || fail "Docker Compose ${MINIMUM_COMPOSE_VERSION} or newer is required; found ${compose_version}"
}

check_existing_installation() {
  if [ ! -e "${INSTALL_DIR}" ]; then
    return
  fi
  [ -d "${INSTALL_DIR}" ] || fail "installation target exists and is not a directory: ${INSTALL_DIR}"
  if [ -f "${INSTALL_DIR}/${MARKER_FILE}" ] && [ -f "${INSTALL_DIR}/docker-compose.prod.yml" ]; then
    if docker compose -f "${INSTALL_DIR}/docker-compose.prod.yml" --env-file "${INSTALL_DIR}/.env" ps --status running --services 2>/dev/null | grep -qx consolehub; then
      printf 'Console Hub is already installed and running in %s.\n' "${INSTALL_DIR}"
      printf 'Update with: cd %s && docker compose -f docker-compose.prod.yml up -d --pull always --wait\n' "${INSTALL_DIR}"
      exit 0
    fi
    fail "an existing Console Hub installation was found but is not healthy: ${INSTALL_DIR}"
  fi
  fail "refusing to overwrite an existing directory: ${INSTALL_DIR}"
}

download_assets() {
  if [ "${TESTING}" != 1 ]; then
    case "${ASSET_BASE_URL}" in
      https://*) ;;
      *) fail 'installer assets must be downloaded over HTTPS' ;;
    esac
  fi

  TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/consolehub-install.XXXXXX")
  if [ "${TESTING}" = 1 ]; then
    curl --fail --silent --show-error --location "${ASSET_BASE_URL}/docker-compose.prod.yml" -o "${TEMP_DIR}/docker-compose.prod.yml"
    curl --fail --silent --show-error --location "${ASSET_BASE_URL}/docker-compose.prod.yml.sha256" -o "${TEMP_DIR}/docker-compose.prod.yml.sha256"
  else
    curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location "${ASSET_BASE_URL}/docker-compose.prod.yml" -o "${TEMP_DIR}/docker-compose.prod.yml"
    curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location "${ASSET_BASE_URL}/docker-compose.prod.yml.sha256" -o "${TEMP_DIR}/docker-compose.prod.yml.sha256"
  fi
  (cd "${TEMP_DIR}" && sha256sum -c docker-compose.prod.yml.sha256) || fail 'Compose checksum verification failed'
}

write_environment() {
  umask 077
cat > "${TEMP_DIR}/.env" <<EOF
ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
APP_BIND_ADDRESS=${APP_BIND_ADDRESS}
APP_PORT=${APP_PORT}
TZ=${TZ}
EOF
  if [ -n "${TRUSTED_PROXY}" ]; then
    printf 'TRUSTED_PROXY=%s\n' "${TRUSTED_PROXY}" >> "${TEMP_DIR}/.env"
  fi
}

confirm_plan() {
  printf '\nConsole Hub installation plan\n'
  printf '  Directory: %s\n' "${INSTALL_DIR}"
  printf '  Browser URL: %s\n' "${ALLOWED_ORIGINS}"
  if [ -n "${TRUSTED_PROXY}" ]; then printf '  Trusted proxy: %s\n' "${TRUSTED_PROXY}"; fi
  printf '  Bind: %s:%s\n' "${APP_BIND_ADDRESS}" "${APP_PORT}"
  printf '  Timezone: %s\n\n' "${TZ}"

  if [ "${NON_INTERACTIVE}" != 1 ]; then
    printf 'Create this installation and start Console Hub? [y/N]: '
    IFS= read -r answer
    case "${answer}" in y|Y|yes|YES) ;; *) fail 'installation cancelled' ;; esac
  fi
}

install_and_start() {
  docker compose -f "${TEMP_DIR}/docker-compose.prod.yml" --env-file "${TEMP_DIR}/.env" config >/dev/null || fail 'Docker Compose configuration validation failed'

  parent_dir=$(dirname "${INSTALL_DIR}")
  mkdir -p -- "${parent_dir}"
  mkdir -- "${INSTALL_DIR}"
  install -m 0644 "${TEMP_DIR}/docker-compose.prod.yml" "${INSTALL_DIR}/docker-compose.prod.yml"
  install -m 0600 "${TEMP_DIR}/.env" "${INSTALL_DIR}/.env"
  : > "${INSTALL_DIR}/${MARKER_FILE}"
  chmod 0600 "${INSTALL_DIR}/${MARKER_FILE}"

  if ! (cd "${INSTALL_DIR}" && docker compose -f docker-compose.prod.yml up -d --pull always --wait); then
    fail "startup failed; installation files were preserved in ${INSTALL_DIR} for diagnosis"
  fi

  case "${ALLOWED_ORIGINS}" in
    http://*) curl --fail --silent --show-error "${ALLOWED_ORIGINS}/api/health" >/dev/null || fail "container is healthy but the direct HTTP URL is not reachable: ${ALLOWED_ORIGINS}" ;;
  esac

  printf '\nConsole Hub is healthy.\n'
  printf 'Open %s and create the Bootstrap Administrator.\n' "${ALLOWED_ORIGINS}"
}

validate_install_dir
validate_platform
check_existing_installation
prompt_configuration
download_assets
write_environment
confirm_plan
install_and_start
