#!/usr/bin/env bash
#
# WolfPanel Agent installer (v1 development).
#
# Usage:
#   curl -fsSL https://downloads.wolfpanel.net/agent/install.sh | sudo bash
#   curl -fsSL https://downloads.wolfpanel.net/agent/install.sh | sudo bash -s -- --token INSTALL_TOKEN
#   sudo ./install.sh --token dev-token            # local development
#
# v1 notes:
#   * For development the agent is installed from the local checkout (the src/
#     directory next to this script). Production downloads a versioned release
#     package from downloads.wolfpanel.net, verifies its SHA-256 against
#     latest.json, and only then extracts it.
#   * Discovery is read-only; the installer changes only WolfPanel's own dirs.
#   * Registration is best-effort: a registration failure leaves the service
#     installed and retrying, and the installer finishes with a warning.
#
set -euo pipefail

# --- Settings ---------------------------------------------------------------
# Base directories are overridable via env for testing; production defaults are
# unchanged so existing installations remain compatible.
OPT_DIR="${WOLFPANEL_OPT_DIR:-/opt/wolfpanel}"
ETC_DIR="${WOLFPANEL_ETC_DIR:-/etc/wolfpanel}"
VAR_DIR="${WOLFPANEL_VAR_DIR:-/var/lib/wolfpanel}"
LOG_DIR="${WOLFPANEL_LOG_DIR:-/var/log/wolfpanel}"
VERSION_DIR=""  # resolved dynamically in main
RELEASE_BASE="${WOLFPANEL_RELEASE_BASE:-https://downloads.wolfpanel.net}"
RELEASE_CHANNEL="${WOLFPANEL_RELEASE_CHANNEL:-stable}"

INSTALL_TOKEN=""
WOLFPANEL_API_URL="${WOLFPANEL_API_URL:-}"
ORIG_ENV_API_URL="${WOLFPANEL_API_URL}"
CLI_API_URL=""
FORCE_CONFIG=false
VERSION=""
RELEASE_DIR=""

# Resolved from latest.json on standalone installs.
MANIFEST_SHA256=""
MANIFEST_FILE=""

# --- Helpers ----------------------------------------------------------------
log()  { echo "[wolfpanel] $*"; }
warn() { echo "[wolfpanel][warning] $*" >&2; }
err()  { echo "[wolfpanel][error] $*" >&2; }
die()  { err "$*"; exit 1; }

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --token) INSTALL_TOKEN="${2:-}"; shift 2 ;;
      --token=*) INSTALL_TOKEN="${1#*=}"; shift ;;
      --api-url) CLI_API_URL="${2:-}"; shift 2 ;;
      --api-url=*) CLI_API_URL="${1#*=}"; shift ;;
      --force-config) FORCE_CONFIG=true; shift ;;
      *) die "unknown argument: $1" ;;
    esac
  done
}

# Compute the SHA-256 of a file using whichever tool is available.
sha256_of() {
  local f="$1"
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$f" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$f" | awk '{print $1}'
  elif command -v openssl >/dev/null 2>&1; then
    openssl dgst -sha256 "$f" | awk '{print $NF}'
  else
    return 1
  fi
}

# Extract a string field from a flat JSON manifest (no jq dependency).
json_field() {
  local key="$1" file="$2"
  grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$file" \
    | head -n1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/'
}

# Decode hex string to binary file using python or fallback commands.
decode_hex_to_bin() {
  local hex="$1" dest="$2"
  if command -v python3 >/dev/null 2>&1; then
    python3 -c 'import sys; sys.stdout.buffer.write(bytes.fromhex(sys.argv[1]))' "$hex" > "$dest"
  elif command -v python >/dev/null 2>&1; then
    python -c 'import sys; sys.stdout.buffer.write(bytes.fromhex(sys.argv[1]))' "$hex" > "$dest"
  elif command -v xxd >/dev/null 2>&1; then
    echo -n "$hex" | xxd -r -p > "$dest"
  elif command -v perl >/dev/null 2>&1; then
    perl -e 'print pack("H*", $ARGV[0])' "$hex" > "$dest"
  else
    return 1
  fi
}

download_to() {
  local url="$1" dest="$2"
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL -o "$dest" "$url"
  elif command -v wget >/dev/null 2>&1; then
    wget -q -O "$dest" "$url"
  else
    die "neither curl nor wget found; cannot download ${url}"
  fi
}

resolve_api_url() {
  if [[ -n "${CLI_API_URL}" ]]; then
    WOLFPANEL_API_URL="${CLI_API_URL}"
  elif [[ -n "${ORIG_ENV_API_URL}" ]]; then
    WOLFPANEL_API_URL="${ORIG_ENV_API_URL}"
  elif [[ -f "${ETC_DIR}/agent.conf" ]] && [[ "${FORCE_CONFIG}" == "false" ]]; then
    # Extract from existing config
    local file_val
    file_val=$(grep -E "^WOLFPANEL_API_URL=" "${ETC_DIR}/agent.conf" | head -n1 | cut -d'=' -f2- | tr -d '\047" ')
    if [[ -n "${file_val}" ]]; then
      WOLFPANEL_API_URL="${file_val}"
    else
      WOLFPANEL_API_URL="https://api.wolfpanel.net"
    fi
  else
    WOLFPANEL_API_URL="https://api.wolfpanel.net"
  fi
}

require_root() {
  [[ "$(id -u)" -eq 0 ]] || die "must run as root (use sudo)"
}

detect_platform() {
  OS="$(uname -s)"
  ARCH="$(uname -m)"
  log "detected platform: ${OS} ${ARCH}"
  # systemd is required, so an unsupported platform must hard-fail rather than
  # continue with only a warning.
  [[ "$OS" == "Linux" ]] || die "unsupported platform: ${OS}. WolfPanel Agent requires Linux with systemd."
}

create_dirs() {
  log "creating directory structure"
  mkdir -p "${VERSION_DIR}" "${ETC_DIR}/secrets" \
           "${VAR_DIR}/spool" "${VAR_DIR}/jobs" "${VAR_DIR}/discovery" "${VAR_DIR}/cache" \
           "${LOG_DIR}"
  chmod 0700 "${ETC_DIR}" || true
  chmod 0700 "${ETC_DIR}/secrets" || true
  chmod 0700 "${VAR_DIR}" || true
  chmod 0700 "${LOG_DIR}" || true
}

install_agent_files() {
  local script_dir
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"

  if [[ -d "${script_dir}/src" ]]; then
    # Development install from the local checkout.
    log "installing agent from local checkout"
    cp -r "${script_dir}/src" "${VERSION_DIR}/src"

    # Copy helper files that actually exist in the repository.
    for f in VERSION README.md config systemd deploy; do
      if [[ -e "${script_dir}/${f}" ]]; then
        cp -r "${script_dir}/${f}" "${VERSION_DIR}/"
      fi
    done
  else
    # Standalone install: download, VERIFY, then extract the release package.
    local temp_tar
    local url
    if [[ -n "${DOWNLOAD_URL}" ]]; then
      url="${DOWNLOAD_URL}"
      temp_tar="${VAR_DIR}/cache/agent.tar.gz"
    else
      local artifact="${MANIFEST_FILE:-latest.tar.gz}"
      url="${RELEASE_BASE}/agent/${RELEASE_CHANNEL}/${artifact}"
      temp_tar="${VAR_DIR}/cache/${artifact}"
    fi

    mkdir -p "${VAR_DIR}/cache"
    log "downloading release package from ${url}"
    download_to "${url}" "${temp_tar}"

    # Integrity verification: refuse to extract an unverified tarball.
    if [[ -n "${MANIFEST_SHA256}" ]]; then
      local actual
      actual="$(sha256_of "${temp_tar}")" || die "no sha256 tool available to verify download"
      if [[ "${actual}" != "${MANIFEST_SHA256}" ]]; then
        rm -f "${temp_tar}"
        die "checksum mismatch for ${artifact}: expected ${MANIFEST_SHA256}, got ${actual}. Aborting before extraction."
      fi
      log "checksum verified: ${actual}"
    else
      rm -f "${temp_tar}"
      die "latest.json did not provide a sha256; refusing to extract unverified package"
    fi

    # Cryptographic Signature Verification
    if [[ -n "${MANIFEST_SIGNATURE:-}" ]]; then
      command -v openssl >/dev/null 2>&1 || die "openssl is required to verify release signatures"
      
      local pubkey_file
      pubkey_file="${VAR_DIR}/cache/release_pubkey.pem"
      cat > "${pubkey_file}" <<'PEMEOF'
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAXRRP9DHJGs04GyapFbyK
QWMlRIKCcQue7Tj4gRXKc0JAXxaNnnHOX6LFXEMHpr1StIKjCU6NeUOwnzZiX5Px
xssQXxbIPCukZCGeYVi7SeXf6A77rjcQZJxsraJS0FHB7AoqMwlfVna5rX2cnUW8
1weBnGHenxE2unxcSsVsQbyoIi/0qQHhd8uJroxlGTlL176q/MetC9Nwhb0BiDwk
uw38K26Hcav6sD6JYUjmUnAiBRObw2AleHCQDQgeatVquGDya6IMraY0/tOC+Jwd
grzq5HSClLh1mIkRLHIeIDpKtTOahm52fu2Dl1cw8mhl3EgOh8PIKWefPbgyjb4w
1QIDAQAB
-----END PUBLIC KEY-----
PEMEOF

      local sig_file
      sig_file="${VAR_DIR}/cache/release_sig.bin"
      decode_hex_to_bin "${MANIFEST_SIGNATURE}" "${sig_file}" || {
        rm -f "${temp_tar}" "${pubkey_file}"
        die "failed to decode release signature hex"
      }

      if ! openssl dgst -sha256 -verify "${pubkey_file}" -signature "${sig_file}" "${temp_tar}" >/dev/null 2>&1; then
        rm -f "${temp_tar}" "${pubkey_file}" "${sig_file}"
        die "Signature verification failed: release package signature is invalid! Aborting before extraction."
      fi
      log "cryptographic signature verified successfully"
      rm -f "${pubkey_file}" "${sig_file}"
    else
      rm -f "${temp_tar}"
      die "latest.json did not provide a signature; refusing to install unverified package"
    fi

    log "extracting release package to ${VERSION_DIR}"
    tar -xzf "${temp_tar}" -C "${VERSION_DIR}"
    rm -f "${temp_tar}"

    # Cross-check: the extracted VERSION must match the manifest version, so we
    # never populate versions/<new> with old code (version/content mismatch).
    if [[ -f "${VERSION_DIR}/VERSION" ]]; then
      local extracted_version
      extracted_version="$(tr -d '\r\n[:space:]' < "${VERSION_DIR}/VERSION")"
      if [[ "${extracted_version}" != "${VERSION}" ]]; then
        die "version mismatch: manifest says ${VERSION} but extracted package is ${extracted_version}. Aborting."
      fi
      log "extracted VERSION matches manifest: ${extracted_version}"
    else
      die "extracted package is missing its VERSION file; aborting"
    fi
  fi

  # RELEASE_DIR is the resolved version directory.
  RELEASE_DIR="${VERSION_DIR}"

  # Verification checks
  log "verifying installed files in ${RELEASE_DIR}"
  [[ -f "${RELEASE_DIR}/src/main.py" ]] || die "Verification failed: ${RELEASE_DIR}/src/main.py is missing!"
  [[ -f "${RELEASE_DIR}/systemd/wolfpanel-agent.service" ]] || die "Verification failed: ${RELEASE_DIR}/systemd/wolfpanel-agent.service is missing!"
  [[ -f "${RELEASE_DIR}/VERSION" ]] || die "Verification failed: ${RELEASE_DIR}/VERSION is missing!"

  # Wrapper so systemd / operators call a stable path regardless of runtime.
  cat > "${VERSION_DIR}/wolfpanel-agent" <<'WRAP'
#!/usr/bin/env bash
exec python3 "$(dirname "$0")/src/main.py" "$@"
WRAP
  chmod +x "${VERSION_DIR}/wolfpanel-agent"

  # Atomic-swap-ready symlink: 'current' always points at the live version.
  ln -sfn "${VERSION_DIR}" "${OPT_DIR}/current"
  log "agent files installed at ${VERSION_DIR}"
}

write_config() {
  if [[ -f "${ETC_DIR}/agent.conf" ]] && [[ "${FORCE_CONFIG}" == "false" ]]; then
    log "config already exists; preserving it"
    # The installed VERSION file is the single source of truth for the running
    # build, so drop any stale WOLFPANEL_AGENT_VERSION left in an older config
    # to avoid reporting an outdated version on upgrade.
    if grep -qE "^WOLFPANEL_AGENT_VERSION=" "${ETC_DIR}/agent.conf"; then
      log "removing stale WOLFPANEL_AGENT_VERSION from existing config"
      grep -vE "^WOLFPANEL_AGENT_VERSION=" "${ETC_DIR}/agent.conf" > "${ETC_DIR}/agent.conf.tmp" \
        && mv "${ETC_DIR}/agent.conf.tmp" "${ETC_DIR}/agent.conf"
    fi
    chmod 0600 "${ETC_DIR}/agent.conf" || true
    return
  fi
  log "writing default config"
  # NOTE: only static configuration goes here (this file is also the systemd
  # EnvironmentFile). Runtime status/error/version live in state.json /
  # the VERSION file and must NOT be written here.
  cat > "${ETC_DIR}/agent.conf" <<EOF
WOLFPANEL_API_URL=${WOLFPANEL_API_URL}
WOLFPANEL_RELEASE_CHANNEL=${RELEASE_CHANNEL}
WOLFPANEL_SERVER_ID=
WOLFPANEL_HEARTBEAT_INTERVAL=60
EOF
  chmod 0600 "${ETC_DIR}/agent.conf" || true
}

write_pending_state() {
  # Persist a clear pending state so the running service knows registration is
  # still owed and keeps retrying (without crash-looping).
  local reason="$1"
  mkdir -p "${VAR_DIR}"
  cat > "${VAR_DIR}/state.json" <<EOF
{
  "status": "pending_install",
  "last_error": "${reason}",
  "registration_pending": true,
  "revoked": false
}
EOF
}

register_agent() {
  log "Using Central API URL: ${WOLFPANEL_API_URL}"

  if [[ -z "${WOLFPANEL_API_URL}" ]] || [[ ! "${WOLFPANEL_API_URL}" =~ ^https?:// ]]; then
    warn "Invalid WOLFPANEL_API_URL: '${WOLFPANEL_API_URL}'. Skipping registration."
    write_pending_state "invalid api url"
    return 1
  fi

  if [[ -z "${INSTALL_TOKEN}" ]]; then
    # Pairing-flow placeholder (browser pairing not implemented in v1).
    cat <<EOF

No install token provided.

Browser pairing is not implemented in v1. Re-run with --token once you have an
install token to complete registration.

EOF
    log "skipping registration (no token)"
    write_pending_state "no install token provided"
    return 1
  fi

  log "registering agent with Central API"
  # The install token is passed through and used in-memory only; the agent
  # never writes it to disk. Registration is best-effort and must not abort the
  # whole install.
  if WOLFPANEL_API_URL="${WOLFPANEL_API_URL}" "${OPT_DIR}/current/wolfpanel-agent" register --token "${INSTALL_TOKEN}"; then
    log "registration succeeded"
    return 0
  fi

  warn "registration failed; the service will keep retrying. Re-run with --token to retry manually."
  write_pending_state "registration failed during install"
  return 1
}

install_service() {
  local unit_src
  unit_src="${RELEASE_DIR}/systemd/wolfpanel-agent.service"

  # systemd is required for the agent to run.
  command -v systemctl >/dev/null 2>&1 || die "systemctl not found; systemd is required to run WolfPanel Agent"
  [[ -f "${unit_src}" ]] || die "service template not found at ${unit_src}"

  log "installing systemd service"
  cp "${unit_src}" /etc/systemd/system/wolfpanel-agent.service
  systemctl daemon-reload
  systemctl enable wolfpanel-agent.service
  systemctl restart wolfpanel-agent.service
  log "service enabled and started"
}

resolve_version() {
  local script_dir
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"

  if [[ -d "${script_dir}/src" ]]; then
    # Local dev install: VERSION file is the source of truth.
    if [[ -f "${script_dir}/VERSION" ]]; then
      VERSION=$(tr -d '\r\n[:space:]' < "${script_dir}/VERSION")
    else
      VERSION="dev"
    fi
  else
    # Standalone install: fetch the manifest and read version + sha256 + file.
    log "fetching latest version manifest from ${RELEASE_BASE}/agent/${RELEASE_CHANNEL}/latest.json"
    mkdir -p "${VAR_DIR}/cache"
    local temp_manifest="${VAR_DIR}/cache/latest.json"
    download_to "${RELEASE_BASE}/agent/${RELEASE_CHANNEL}/latest.json" "${temp_manifest}" \
      || die "failed to download latest.json"

    VERSION="$(json_field version "${temp_manifest}")"
    MANIFEST_SHA256="$(json_field checksum_sha256 "${temp_manifest}")"
    if [[ -z "${MANIFEST_SHA256}" ]]; then
      MANIFEST_SHA256="$(json_field sha256 "${temp_manifest}")"
    fi
    DOWNLOAD_URL="$(json_field download_url "${temp_manifest}")"
    if [[ -z "${DOWNLOAD_URL}" ]]; then
      MANIFEST_FILE="$(json_field file "${temp_manifest}")"
    fi
    MANIFEST_SIGNATURE="$(json_field signature "${temp_manifest}")"
    rm -f "${temp_manifest}"

    [[ -n "${VERSION}" ]] || die "failed to parse version from manifest"
  fi

  RELEASE_DIR="${OPT_DIR}/versions/${VERSION}"
  VERSION_DIR="${RELEASE_DIR}"
  log "resolved version: ${VERSION}"
}

main() {
  parse_args "$@"
  require_root
  detect_platform
  resolve_version
  resolve_api_url
  create_dirs
  install_agent_files
  write_config
  # Order per design: install + enable + start service first, THEN register.
  install_service
  local reg_ok=true
  register_agent || reg_ok=false

  if [[ "${reg_ok}" == "true" ]]; then
    # Reload the running service so it picks up freshly stored credentials.
    systemctl restart wolfpanel-agent.service || true
  fi

  cat <<EOF

WolfPanel Agent installation complete.
  version:  ${VERSION}
  binary:   ${OPT_DIR}/current/wolfpanel-agent
  config:   ${ETC_DIR}/agent.conf
  state:    ${VAR_DIR}/state.json
  logs:     ${LOG_DIR}/agent.log

Check status:  ${OPT_DIR}/current/wolfpanel-agent status
EOF

  if [[ "${reg_ok}" != "true" ]]; then
    warn "Installation finished, but registration is PENDING."
    warn "The service is running and will retry. Complete it with:"
    warn "  sudo ${OPT_DIR}/current/wolfpanel-agent register --token <INSTALL_TOKEN>"
  fi
}

# Run main only when executed directly; sourcing exposes functions for testing.
if [[ "${BASH_SOURCE[0]:-$0}" == "${0}" ]]; then
  main "$@"
fi
