#!/bin/zsh
# FileWave Let's Encrypt installer (macOS)
# Supports:
#   - HTTP-01 validation (standalone, requires inbound TCP/80)
#   - DNS-01 validation via Cloudflare plugin (no TCP/80 requirement)
#
# UX-focused macOS script with modular challenge handlers.

set -euo pipefail

LOG_FILE="/var/log/filewave-letsencrypt.log"
RENEW_HOOK="/etc/letsencrypt/renewal-hooks/deploy/filewave-server-cert.sh"
CF_SECRETS_DIR="/etc/letsencrypt/secrets"
CF_CREDENTIALS_FILE="${CF_SECRETS_DIR}/cloudflare.ini"
LEGACY_CRON_FILE="/etc/cron.daily/letsencrypt-filewave"
LAUNCHD_LABEL="com.filewave.letsencrypt.renew"
LAUNCHD_PLIST="/Library/LaunchDaemons/${LAUNCHD_LABEL}.plist"
RENEW_RUNNER="/usr/local/filewave/sbin/filewave-letsencrypt-renew.zsh"
RENEW_LOG_FILE="/var/log/filewave-letsencrypt-renew.log"

METHOD=""
HOSTNAME=""
EMAIL=""
CLOUDFLARE_TOKEN=""
OS_VERSION="unknown"
STEP_INDEX=0
TOTAL_STEPS=10
DNS_TOOL=""
CERTBOT_BIN=""

# ---------- Output formatting (no dependencies) ----------
if [ -t 1 ] && [ "${NO_COLOR:-}" = "" ]; then
  C_RESET='\033[0m'
  C_BOLD='\033[1m'
  C_DIM='\033[2m'
  C_BLUE='\033[34m'
  C_GREEN='\033[32m'
  C_YELLOW='\033[33m'
  C_RED='\033[31m'
else
  C_RESET=''
  C_BOLD=''
  C_DIM=''
  C_BLUE=''
  C_GREEN=''
  C_YELLOW=''
  C_RED=''
fi

banner() {
  echo
  echo -e "${C_BOLD}==============================================================${C_RESET}"
  echo -e "${C_BOLD}  FileWave Let's Encrypt Installer (macOS)${C_RESET}"
  echo -e "${C_BOLD}==============================================================${C_RESET}"
  echo
}

log() {
  # Always log plain text (no ANSI codes) to file.
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

info() { echo -e "${C_BLUE}[INFO]${C_RESET} $*"; }
ok()   { echo -e "${C_GREEN}[OK]${C_RESET}   $*"; }
warn() { echo -e "${C_YELLOW}[WARN]${C_RESET} $*"; }
err()  { echo -e "${C_RED}[ERR]${C_RESET}  $*"; }

step() {
  STEP_INDEX=$((STEP_INDEX + 1))
  echo
  echo -e "${C_BOLD}[${STEP_INDEX}/${TOTAL_STEPS}]${C_RESET} $*"
}

die() {
  err "$*"
  log "ERROR: $*"
  exit 1
}

usage() {
  cat <<USAGE
FileWave Let's Encrypt installer for macOS.

Usage:
  sudo ./filewave-letsencrypt-macos.zsh --install
  sudo ./filewave-letsencrypt-macos.zsh --uninstall

Install flow prompts for:
  - FQDN
  - email
  - challenge method:
      1) HTTP-01 (standalone)
      2) DNS-01 (Cloudflare)

Notes:
  - HTTP-01 requires inbound TCP/80 reachability.
  - DNS-01 (Cloudflare) requires an API token with DNS edit rights.
USAGE
}

# ---------- Validation + helpers ----------
require_root() {
  if [ "$(id -u)" -ne 0 ]; then
    echo "root rights required - please rerun as"
    echo "sudo $0 --install"
    exit 1
  fi
}

require_macos() {
  [ "$(uname -s)" = "Darwin" ] || die "This script is designed to run on macOS"
  OS_VERSION=$(sw_vers -productVersion 2>/dev/null || echo unknown)

  local major
  major="${OS_VERSION%%.*}"
  [[ "$major" =~ ^[0-9]+$ ]] || die "Unable to parse macOS version: $OS_VERSION"

  if [ "$major" -lt 14 ]; then
    die "Unsupported macOS version: $OS_VERSION. This script supports macOS 14 and newer."
  fi

  ok "macOS $OS_VERSION detected (supported: 14+)"
}

ensure_filewave_server() {
  [ -x /usr/local/bin/fwcontrol ] || die "FileWave control binary not found: /usr/local/bin/fwcontrol"
  [ -d /usr/local/filewave/certs ] || die "FileWave cert directory not found: /usr/local/filewave/certs"

  if [ ! -x /usr/local/filewave/python/bin/python ]; then
    warn "FileWave python not found at /usr/local/filewave/python/bin/python (update_dep_profile_certs may not run)"
  fi
}

validate_hostname() {
  local hostname="$1"
  [[ "$hostname" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$ ]] || return 1
  [ ${#hostname} -le 253 ] || return 1
  return 0
}

validate_email() {
  local email="$1"
  [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] || return 1
  return 0
}

prompt_yes_no() {
  local prompt="$1"
  local answer
  while true; do
    read -r "answer?$prompt [yes/no]: "
    case "${answer:l}" in
      yes|y) return 0 ;;
      no|n) return 1 ;;
      *) warn "Please answer yes or no." ;;
    esac
  done
}

# ---------- Package/tool setup ----------
ensure_dns_tool() {
  if command -v nslookup >/dev/null 2>&1; then
    DNS_TOOL="nslookup"
    return 0
  fi

  if command -v dig >/dev/null 2>&1; then
    DNS_TOOL="dig"
    return 0
  fi

  die "Neither nslookup nor dig is available; install DNS tools and retry"
}

check_public_dns_resolution() {
  local hostname="$1"
  ensure_dns_tool

  if [ "$DNS_TOOL" = "nslookup" ]; then
    if nslookup "$hostname" 8.8.8.8 >/dev/null 2>&1; then
      ok "Public DNS resolution check passed for $hostname via 8.8.8.8"
      return 0
    fi

    warn "$hostname did not resolve via 8.8.8.8; trying system resolver"
    nslookup "$hostname" >/dev/null 2>&1 || die "$hostname did not resolve via 8.8.8.8 or system resolver"
  else
    if [ -n "$(dig +short @8.8.8.8 "$hostname" 2>/dev/null)" ]; then
      ok "Public DNS resolution check passed for $hostname via 8.8.8.8"
      return 0
    fi

    warn "$hostname did not resolve via 8.8.8.8; trying system resolver"
    [ -n "$(dig +short "$hostname" 2>/dev/null)" ] || die "$hostname did not resolve via 8.8.8.8 or system resolver"
  fi

  ok "DNS resolution check passed for $hostname via system resolver"
}

update_system_packages() {
  info "macOS detected; skipping apt-based package update"
  warn "No apt on macOS. Ensure system updates are applied via Software Update."
}

homebrew_install_hint() {
  echo
  warn "Homebrew is required on macOS for this install path."
  echo "Install Homebrew from: https://brew.sh"
  echo "Use the exact install command shown there, then re-run this script."
  echo
}

brew_as_invoking_user() {
  local invoking_user="${SUDO_USER:-}"

  if [ -z "$invoking_user" ] || [ "$invoking_user" = "root" ]; then
    homebrew_install_hint
    die "This script was launched as root directly. Re-run with sudo from a regular macOS admin account so Homebrew can run as that user."
  fi

  sudo -u "$invoking_user" "$@"
}

resolve_certbot_bin() {
  if command -v certbot >/dev/null 2>&1; then
    CERTBOT_BIN="$(command -v certbot)"
    return 0
  fi

  for p in /opt/homebrew/bin/certbot /usr/local/bin/certbot; do
    if [ -x "$p" ]; then
      CERTBOT_BIN="$p"
      return 0
    fi
  done

  CERTBOT_BIN=""
  return 1
}

ensure_certbot_base() {
  if resolve_certbot_bin; then
    ok "certbot already present at $CERTBOT_BIN"
    return 0
  fi

  if command -v brew >/dev/null 2>&1; then
    info "Installing certbot via Homebrew (non-root user context)..."
    brew_as_invoking_user brew install certbot || die "Homebrew certbot install failed"
  else
    homebrew_install_hint
    die "certbot not found and Homebrew is not installed"
  fi

  resolve_certbot_bin || die "Certbot installation failed"
  ok "certbot installed at $CERTBOT_BIN"
}

ensure_cloudflare_plugin() {
  info "Ensuring certbot-dns-cloudflare plugin is available..."

  "$CERTBOT_BIN" plugins 2>/dev/null | grep -qi 'dns-cloudflare' && {
    ok "Cloudflare DNS plugin already available"
    return 0
  }

  if command -v brew >/dev/null 2>&1; then
    local brew_prefix certbot_python
    brew_prefix="$(brew_as_invoking_user brew --prefix certbot 2>/dev/null || true)"
    certbot_python="${brew_prefix}/libexec/bin/python3"

    if [ -x "$certbot_python" ]; then
      info "Installing plugin with Certbot's bundled Python (Let's Encrypt recommended for macOS Homebrew installs)..."
      brew_as_invoking_user "$certbot_python" -m pip install --upgrade certbot-dns-cloudflare || die "Failed to install certbot-dns-cloudflare via Homebrew certbot Python"
    fi
  fi

  if ! "$CERTBOT_BIN" plugins 2>/dev/null | grep -qi 'dns-cloudflare'; then
    if command -v pipx >/dev/null 2>&1 && pipx list 2>/dev/null | grep -qi 'package certbot'; then
      info "Detected pipx-managed certbot; trying pipx inject certbot-dns-cloudflare..."
      pipx inject certbot certbot-dns-cloudflare || true
    fi
  fi

  "$CERTBOT_BIN" plugins 2>/dev/null | grep -qi 'dns-cloudflare' || {
    if command -v brew >/dev/null 2>&1; then
      die "Cloudflare DNS plugin not detected. On macOS with Homebrew Certbot, run: $(brew --prefix certbot)/libexec/bin/python3 -m pip install certbot-dns-cloudflare"
    fi

    homebrew_install_hint
    die "Cloudflare DNS plugin not detected and Homebrew-based plugin install path is unavailable"
  }

  ok "Cloudflare DNS plugin available"
}

# ---------- Cert + FileWave tasks ----------
setup_cloudflare_credentials() {
  mkdir -p "$CF_SECRETS_DIR"
  chmod 700 "$CF_SECRETS_DIR"

  cat > "$CF_CREDENTIALS_FILE" <<CRED
# Cloudflare token used by certbot dns-cloudflare plugin
dns_cloudflare_api_token = $CLOUDFLARE_TOKEN
CRED

  chmod 600 "$CF_CREDENTIALS_FILE"

  [ -f "$CF_CREDENTIALS_FILE" ] || die "Failed to create $CF_CREDENTIALS_FILE"
  [ "$(stat -f '%Lp' "$CF_CREDENTIALS_FILE")" = "600" ] || die "$CF_CREDENTIALS_FILE permission should be 600"
  ok "Cloudflare credentials created ($CF_CREDENTIALS_FILE, mode 600)"
}

verify_cloudflare_credentials_exists() {
  [ -f "$CF_CREDENTIALS_FILE" ] || die "Cloudflare credentials file missing: $CF_CREDENTIALS_FILE"
  [ -r "$CF_CREDENTIALS_FILE" ] || die "Cloudflare credentials file unreadable: $CF_CREDENTIALS_FILE"
}

backup_existing_certs() {
  local backup_date
  backup_date=$(date +%Y-%m-%d-%H-%M)
  local backup_dir="/usr/local/filewave/certs/backup-${backup_date}"

  mkdir -p "$backup_dir"
  if cp -p /usr/local/filewave/certs/server.* "$backup_dir" 2>/dev/null; then
    ok "Existing certificates backed up to $backup_dir"
  else
    warn "Existing certificates not found for backup (continuing)"
  fi
}

request_certificate_http() {
  info "Requesting certificate via HTTP-01 (standalone)..."
  "$CERTBOT_BIN" -n --agree-tos --standalone certonly -d "$HOSTNAME" -m "$EMAIL" || {
    echo
    err "Certificate request failed (HTTP-01)."
    echo "Likely causes:"
    echo "  - TCP/80 not reachable from internet"
    echo "  - DNS record not pointing to this server"
    echo
    echo "Manual retry:"
    echo "sudo certbot -n --agree-tos --standalone certonly -d \"$HOSTNAME\" -m \"$EMAIL\""
    die "Stopping after HTTP-01 failure"
  }
  ok "Certificate issued (HTTP-01)"
}

request_certificate_dns_cloudflare() {
  verify_cloudflare_credentials_exists

  info "Requesting certificate via DNS-01 (Cloudflare)..."
  "$CERTBOT_BIN" -n --agree-tos --dns-cloudflare --dns-cloudflare-credentials "$CF_CREDENTIALS_FILE" certonly -d "$HOSTNAME" -m "$EMAIL" || {
    echo
    err "Certificate request failed (DNS-01 Cloudflare)."
    echo "Likely causes:"
    echo "  - invalid Cloudflare token"
    echo "  - token lacks DNS edit permission on this zone"
    echo "  - FQDN not in a zone accessible by that token"
    echo
    echo "Manual retry:"
    echo "sudo certbot -n --agree-tos --dns-cloudflare --dns-cloudflare-credentials $CF_CREDENTIALS_FILE certonly -d \"$HOSTNAME\" -m \"$EMAIL\""
    die "Stopping after DNS-01 failure"
  }
  ok "Certificate issued (DNS-01 Cloudflare)"
}

set_mdm_cert_trusted() {
  info "Updating iOS preferences in PostgreSQL (mdm_cert_trusted=true)..."

  local psql_bin="/usr/local/filewave/postgresql/bin/psql"
  if [ ! -x "$psql_bin" ]; then
    warn "PostgreSQL binary not found at $psql_bin (skipping mdm_cert_trusted update)"
    return 0
  fi

  if "$psql_bin" -d mdm -U django -c \
    "INSERT INTO ios_preferences (key, value) VALUES ('mdm_cert_trusted', TRUE) ON CONFLICT (key) DO NOTHING; UPDATE ios_preferences SET value='true' WHERE key='mdm_cert_trusted';" \
    2>/dev/null; then
    ok "iOS preferences updated"
  else
    warn "Failed to update iOS preferences (continuing)"
  fi
}

create_renew_hook() {
  mkdir -p "$(dirname "$RENEW_HOOK")"
  rm -f "$RENEW_HOOK"

  cat > "$RENEW_HOOK" <<HOOK
#!/bin/zsh
set -euo pipefail
HOSTNAME="$HOSTNAME"

if [ -e /usr/local/filewave/certs/server.crt ]; then
  CERT_OWNER=\$(stat -f '%Su' /usr/local/filewave/certs/server.crt 2>/dev/null || echo root)
  CERT_GROUP=\$(stat -f '%Sg' /usr/local/filewave/certs/server.crt 2>/dev/null || echo wheel)
else
  CERT_OWNER=\$(stat -f '%Su' /usr/local/filewave/certs 2>/dev/null || echo root)
  CERT_GROUP=\$(stat -f '%Sg' /usr/local/filewave/certs 2>/dev/null || echo wheel)
fi

cp /etc/letsencrypt/live/"\$HOSTNAME"/fullchain.pem /usr/local/filewave/certs/server.crt
cp /etc/letsencrypt/live/"\$HOSTNAME"/privkey.pem /usr/local/filewave/certs/server.key
chown "\$CERT_OWNER":"\$CERT_GROUP" /usr/local/filewave/certs/server.*

/usr/local/bin/fwcontrol server restart
(yes 2>/dev/null) | /usr/local/filewave/python/bin/python /usr/local/filewave/django/manage.pyc update_dep_profile_certs 2>/dev/null || true
HOOK

  chmod +x "$RENEW_HOOK"
  zsh -n "$RENEW_HOOK" || die "Renewal hook syntax check failed"
  ok "Renewal hook created: $RENEW_HOOK"
}

inject_certificate_now() {
  info "Injecting certificate into FileWave now..."
  "$RENEW_HOOK" || die "Initial certificate injection failed"
  ok "Certificate injected into FileWave"
}

create_renew_runner() {
  mkdir -p "$(dirname "$RENEW_RUNNER")"

  cat > "$RENEW_RUNNER" <<RUNNER
#!/bin/zsh
set -euo pipefail
sleep \$((RANDOM % 7200))
"$CERTBOT_BIN" renew --quiet
RUNNER

  chmod 700 "$RENEW_RUNNER"
  zsh -n "$RENEW_RUNNER" || die "Renewal runner syntax check failed"
  ok "Renewal runner created: $RENEW_RUNNER"
}

create_renew_launchd() {
  cat > "$LAUNCHD_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>${LAUNCHD_LABEL}</string>

  <key>ProgramArguments</key>
  <array>
    <string>${RENEW_RUNNER}</string>
  </array>

  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key>
    <integer>0</integer>
    <key>Minute</key>
    <integer>0</integer>
  </dict>

  <key>RunAtLoad</key>
  <false/>

  <key>StandardOutPath</key>
  <string>${RENEW_LOG_FILE}</string>
  <key>StandardErrorPath</key>
  <string>${RENEW_LOG_FILE}</string>
</dict>
</plist>
PLIST

  chmod 644 "$LAUNCHD_PLIST"

  # Replace existing loaded job if present.
  launchctl bootout "system/${LAUNCHD_LABEL}" 2>/dev/null || true
  launchctl bootout system "$LAUNCHD_PLIST" 2>/dev/null || true
  launchctl bootstrap system "$LAUNCHD_PLIST" || die "Failed to load launchd job"

  launchctl print "system/${LAUNCHD_LABEL}" >/dev/null 2>&1 || die "launchd job did not register correctly"
  ok "LaunchDaemon installed and loaded: $LAUNCHD_PLIST"
}

remove_renew_launchd() {
  launchctl bootout "system/${LAUNCHD_LABEL}" 2>/dev/null || true
  launchctl bootout system "$LAUNCHD_PLIST" 2>/dev/null || true
  rm -f "$LAUNCHD_PLIST"
  rm -f "$RENEW_RUNNER"
  ok "Removed LaunchDaemon and renewal runner"
}

uninstall_files() {
  step "Removing FileWave Let's Encrypt integration files"
  log "Starting uninstall flow"

  rm -f "$RENEW_HOOK"
  rm -f "$LEGACY_CRON_FILE"
  remove_renew_launchd

  if [ -f "$CF_CREDENTIALS_FILE" ]; then
    if command -v shred >/dev/null 2>&1; then
      shred -vfz -n 3 "$CF_CREDENTIALS_FILE" || rm -f "$CF_CREDENTIALS_FILE"
    else
      rm -f "$CF_CREDENTIALS_FILE"
    fi
    ok "Removed Cloudflare credential file"
  else
    info "No Cloudflare credential file found (nothing to remove)"
  fi

  ok "Uninstall complete"
  echo
  echo "certbot package was intentionally left installed."
}

# ---------- Prompting ----------
prompt_hostname() {
  while true; do
    read -r "HOSTNAME?Enter fully qualified domain name (FQDN): "
    if validate_hostname "$HOSTNAME"; then
      return 0
    fi
    warn "Invalid hostname format. Example: fw.example.com"
  done
}

prompt_email() {
  while true; do
    read -r "EMAIL?Enter email address for Let's Encrypt notifications: "
    if validate_email "$EMAIL"; then
      return 0
    fi
    warn "Invalid email format. Example: admin@example.com"
  done
}

prompt_method() {
  while true; do
    echo
    echo "Select Let's Encrypt validation method:"
    echo "  1) HTTP-01 (standalone certbot)"
    echo "     - Requires inbound TCP 80 from internet"
    echo "     - Simpler setup, no DNS API token"
    echo
    echo "  2) DNS-01 (Cloudflare)"
    echo "     - Does NOT require inbound TCP 80"
    echo "     - Requires Cloudflare API token with Zone.DNS edit rights"
    echo
    read -r "method_choice?Choice [1/2]: "

    case "$method_choice" in
      1) METHOD="http"; return 0 ;;
      2) METHOD="dns-cloudflare"; return 0 ;;
      *) warn "Invalid choice. Enter 1 or 2." ;;
    esac
  done
}

prompt_cloudflare_token_if_needed() {
  if [ "$METHOD" = "dns-cloudflare" ]; then
    while true; do
      read -rs "CLOUDFLARE_TOKEN?Enter Cloudflare API token: "
      echo
      if [ -n "$CLOUDFLARE_TOKEN" ]; then
        return 0
      fi
      warn "Cloudflare token cannot be empty."
    done
  fi
}

collect_install_inputs() {
  step "Collecting install inputs"

  prompt_hostname
  prompt_email
  prompt_method
  prompt_cloudflare_token_if_needed

  echo
  echo -e "${C_BOLD}Review before continuing:${C_RESET}"
  echo "  Hostname: $HOSTNAME"
  echo "  Email:    $EMAIL"
  if [ "$METHOD" = "http" ]; then
    echo "  Method:   HTTP-01 (standalone)"
    echo "  Note:     TCP 80 must be publicly reachable"
  else
    echo "  Method:   DNS-01 (Cloudflare)"
    echo "  Note:     Cloudflare token will be stored at $CF_CREDENTIALS_FILE (mode 600)"
  fi

  if ! prompt_yes_no "Proceed with installation"; then
    die "Aborted by user"
  fi
}

print_install_summary() {
  echo
  echo "--------------------------------------------------------------"
  echo -e "${C_BOLD}Install completed successfully${C_RESET}"
  echo "--------------------------------------------------------------"
  echo "Files created/updated:"
  echo "- $RENEW_HOOK"
  echo "- $LAUNCHD_PLIST"
  echo "- $RENEW_RUNNER"
  if [ "$METHOD" = "dns-cloudflare" ]; then
    echo "- $CF_CREDENTIALS_FILE"
  fi
  echo "- $LOG_FILE"
  echo "- $RENEW_LOG_FILE"
  echo

  if [ "$METHOD" = "http" ]; then
    echo "Manual retry (HTTP-01):"
    echo "sudo certbot -n --agree-tos --standalone certonly -d \"$HOSTNAME\" -m \"$EMAIL\""
  else
    echo "Manual retry (DNS-01 Cloudflare):"
    echo "sudo certbot -n --agree-tos --dns-cloudflare --dns-cloudflare-credentials $CF_CREDENTIALS_FILE certonly -d \"$HOSTNAME\" -m \"$EMAIL\""
  fi

  echo
  echo "Optional renewal test:"
  echo "sudo certbot renew --force-renewal"
  echo
}

install_flow() {
  log "Starting install flow"

  collect_install_inputs

  step "Validating FileWave server prerequisites"
  ensure_filewave_server

  step "Validating DNS resolution"
  check_public_dns_resolution "$HOSTNAME"

  step "Backing up existing FileWave certs"
  backup_existing_certs

  step "Updating system packages"
  update_system_packages

  step "Ensuring certbot is installed"
  ensure_certbot_base

  if [ "$METHOD" = "dns-cloudflare" ]; then
    step "Preparing DNS-01 (Cloudflare) prerequisites"
    ensure_cloudflare_plugin
    setup_cloudflare_credentials

    step "Requesting certificate"
    request_certificate_dns_cloudflare
  else
    step "Requesting certificate"
    warn "HTTP-01 requires public reachability on TCP 80 during validation"
    request_certificate_http
  fi

  step "Applying certificate to FileWave"
  set_mdm_cert_trusted
  create_renew_hook
  inject_certificate_now

  step "Configuring auto-renewal"
  create_renew_runner
  create_renew_launchd

  print_install_summary
}

main() {
  local action="${1:-}"

  if [ "$action" != "--install" ] && [ "$action" != "--uninstall" ]; then
    usage
    exit 1
  fi

  require_root
  banner
  require_macos

  case "$action" in
    --install)
      install_flow
      ;;
    --uninstall)
      uninstall_files
      ;;
  esac
}

main "$@"
