#!/usr/bin/env bash
# ============================================================================
# DashCaddy Linux Installer v1.0
#
# Zero-effort install:
#   curl -fsSL https://get.dashcaddy.net | bash
#
# Zero-typing install (local mode, instant access):
#   curl -fsSL https://get.dashcaddy.net | bash -s -- quick
#
# With a domain (only thing you type is the domain):
#   curl -fsSL https://get.dashcaddy.net | bash -s -- --domain my.example.com
#
# Designed for accessibility — all choices are single keystrokes.
# ============================================================================

set -euo pipefail

# ---- Constants -------------------------------------------------------------
readonly DASHCADDY_VERSION="1.1.0"
readonly DASHCADDY_DOWNLOAD="https://get.dashcaddy.net/release/latest.tar.gz"
readonly DASHCADDY_REPO=""  # Set to a git URL to clone instead of downloading
readonly INSTALL_DIR="/etc/dashcaddy"
readonly DOCKER_DATA="/opt/dockerdata"
readonly SITES_DIR="${INSTALL_DIR}/sites"
readonly API_DIR="${SITES_DIR}/dashcaddy-api"
readonly DASHBOARD_DIR="${SITES_DIR}/status"
readonly CONTAINER_NAME="dashcaddy-api"
readonly CADDY_ADMIN_PORT=2019
readonly BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}"
readonly DEFAULT_MAX_STORAGE_BYTES=""

# ---- Tunables (overridable via flags) --------------------------------------
API_PORT=3001
LOCAL_PORT=8080
BACKUP_DIR=""
BACKUP_LIMIT=""

# ---- Runtime state ---------------------------------------------------------
DOMAIN_MODE=""    # public | custom-tld | local
DOMAIN=""
EMAIL=""
TLD=""
CA_NAME="DashCaddy Local CA"
SOURCE_PATH=""
GIT_BRANCH="main"
SKIP_DOCKER=false
SKIP_CADDY=false
UNINSTALL=false
KEEP_CONFIG=false
AUTO_YES=false
QUICK=false
DISTRO=""
DISTRO_FAMILY=""  # debian | rhel | arch
PUBLIC_IP=""
LAN_IP=""
STEP=0
TOTAL_STEPS=7

# ---- Colors ----------------------------------------------------------------
if [[ -t 1 ]]; then
  RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'
  BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'
  DIM='\033[2m'; NC='\033[0m'
else
  RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; DIM=''; NC=''
fi

# ============================================================================
# Output Helpers
# ============================================================================

log()    { echo -e "${CYAN}[DashCaddy]${NC} $*"; }
ok()     { echo -e "${GREEN}  ✓${NC} $*"; }
warn()   { echo -e "${YELLOW}  !${NC} $*"; }
err()    { echo -e "${RED}  ✗${NC} $*" >&2; }
fatal()  { err "$*"; echo -e "${DIM}  If this keeps failing, see: https://dashcaddy.net/docs/troubleshoot${NC}" >&2; exit 1; }

step() {
  STEP=$((STEP + 1))
  echo ""
  echo -e "${BOLD}${BLUE}  [${STEP}/${TOTAL_STEPS}] $*${NC}"
  echo -e "${BLUE}  ─────────────────────────────────────────${NC}"
}

# Simple progress: run command in background, show dots
progress() {
  local msg="$1"; shift
  printf "${CYAN}  ▸${NC} %s " "$msg"

  # Run the command, capture output for error reporting
  local logfile="/tmp/dashcaddy-install-$$.log"
  if "$@" >"$logfile" 2>&1; then
    echo -e "${GREEN}done${NC}"
    rm -f "$logfile"
    return 0
  else
    echo -e "${RED}failed${NC}"
    echo -e "${DIM}  Last output:${NC}"
    tail -5 "$logfile" 2>/dev/null | sed 's/^/    /'
    rm -f "$logfile"
    return 1
  fi
}

elapsed() {
  local diff=$(( $(date +%s) - $1 ))
  if (( diff < 60 )); then
    echo "${diff}s"
  else
    printf '%dm%02ds' $((diff / 60)) $((diff % 60))
  fi
}

# ============================================================================
# System Detection
# ============================================================================

detect_system() {
  # --- Root check ---
  if [[ $EUID -ne 0 ]]; then
    fatal "Must run as root. Try: sudo bash install.sh"
  fi

  # --- OS detection ---
  if [[ ! -f /etc/os-release ]]; then
    fatal "Cannot detect OS — /etc/os-release not found."
  fi
  source /etc/os-release

  case "${ID,,}" in
    ubuntu|debian|pop|linuxmint|elementary|zorin|raspbian)
      DISTRO="${ID}"; DISTRO_FAMILY="debian" ;;
    fedora|rhel|centos|rocky|almalinux|ol|amzn)
      DISTRO="${ID}"; DISTRO_FAMILY="rhel" ;;
    arch|manjaro|endeavouros)
      DISTRO="${ID}"; DISTRO_FAMILY="arch" ;;
    *)
      DISTRO="${ID}"; DISTRO_FAMILY="debian"
      warn "Unknown distro '${ID}' — using Debian-style commands" ;;
  esac

  ok "OS: ${PRETTY_NAME:-$DISTRO}"

  # --- Architecture ---
  local arch
  arch=$(uname -m)
  case "$arch" in
    x86_64|amd64|aarch64|arm64) ok "Arch: ${arch}" ;;
    *) warn "Arch '${arch}' may not be fully supported" ;;
  esac

  # --- Network ---
  LAN_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || hostname -I 2>/dev/null | awk '{print $1}' || echo "unknown")
  PUBLIC_IP=$(curl -fsSL --max-time 3 https://api.ipify.org 2>/dev/null || curl -fsSL --max-time 3 https://ifconfig.me 2>/dev/null || echo "unknown")
  ok "LAN IP: ${LAN_IP}"
  ok "Public IP: ${PUBLIC_IP}"

  # --- Existing install? ---
  if [[ -f "${INSTALL_DIR}/config.json" ]]; then
    warn "Existing DashCaddy installation detected"
    warn "Re-running will upgrade in place (configs preserved)"
  fi
}

# ============================================================================
# Interactive Setup — minimal typing, number keys only
# ============================================================================

interactive_setup() {
  # Skip if mode already set via flags
  if [[ -n "$DOMAIN_MODE" ]]; then return; fi

  # Detect environment to offer smart defaults
  local is_vps=false
  if [[ "$PUBLIC_IP" != "unknown" && "$PUBLIC_IP" != "$LAN_IP" ]] || \
     [[ "$LAN_IP" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then
    : # Private IP — probably homelab or local
  else
    is_vps=true
  fi

  echo ""
  echo -e "${BOLD}  How will you access DashCaddy?${NC}"
  echo ""

  if $is_vps; then
    echo -e "  ${BOLD}1${NC})  Public domain ${DIM}— Let's Encrypt TLS (recommended for VPS)${NC}"
    echo -e "  ${BOLD}2${NC})  Quick start    ${DIM}— http://${PUBLIC_IP}:${LOCAL_PORT} right now, add domain later${NC}"
    echo -e "  ${BOLD}3${NC})  Custom TLD     ${DIM}— Internal CA for homelab (e.g., .home, .lab)${NC}"
  else
    echo -e "  ${BOLD}1${NC})  Quick start    ${DIM}— http://${LAN_IP}:${LOCAL_PORT} right now${NC}"
    echo -e "  ${BOLD}2${NC})  Custom TLD     ${DIM}— Internal CA for homelab (e.g., .home, .lab)${NC}"
    echo -e "  ${BOLD}3${NC})  Public domain  ${DIM}— Let's Encrypt TLS${NC}"
  fi

  echo ""
  local choice
  read -rp "$(echo -e "  ${YELLOW}Press 1, 2, or 3:${NC} ")" -n1 choice
  echo ""

  if $is_vps; then
    case "$choice" in
      1) setup_public_domain ;;
      2) DOMAIN_MODE="local" ;;
      3) setup_custom_tld ;;
      *) DOMAIN_MODE="local"; warn "Defaulting to Quick Start" ;;
    esac
  else
    case "$choice" in
      1) DOMAIN_MODE="local" ;;
      2) setup_custom_tld ;;
      3) setup_public_domain ;;
      *) DOMAIN_MODE="local"; warn "Defaulting to Quick Start" ;;
    esac
  fi
}

setup_public_domain() {
  DOMAIN_MODE="public"
  echo ""
  read -rp "$(echo -e "  ${YELLOW}Domain name:${NC} ")" DOMAIN
  if [[ -z "$DOMAIN" ]]; then
    fatal "Domain is required for public mode."
  fi

  # Auto-check if domain points to this server
  local resolved
  resolved=$(dig +short "$DOMAIN" 2>/dev/null | head -1)
  if [[ -n "$resolved" && "$resolved" == "$PUBLIC_IP" ]]; then
    ok "${DOMAIN} correctly points to ${PUBLIC_IP}"
  elif [[ -n "$resolved" ]]; then
    warn "${DOMAIN} resolves to ${resolved}, but this server is ${PUBLIC_IP}"
    echo -e "  ${DIM}Let's Encrypt will fail if DNS doesn't point here.${NC}"
    echo ""
    read -rp "$(echo -e "  ${YELLOW}Continue anyway? [y/N]:${NC} ")" -n1 cont
    echo ""
    [[ "$cont" =~ ^[Yy]$ ]] || fatal "Fix DNS first, then re-run the installer."
  else
    warn "Could not verify DNS for ${DOMAIN}"
    echo -e "  ${DIM}Make sure ${DOMAIN} points to ${PUBLIC_IP} for TLS to work.${NC}"
  fi

  # Email — offer skip
  echo ""
  echo -e "  ${DIM}Email for Let's Encrypt notifications (optional, press Enter to skip):${NC}"
  read -rp "  " EMAIL
}

setup_custom_tld() {
  DOMAIN_MODE="custom-tld"
  echo ""
  echo -e "  ${DIM}Common choices: .home  .local  .lab  .lan${NC}"
  read -rp "$(echo -e "  ${YELLOW}TLD (default .home):${NC} ")" TLD
  [[ -z "$TLD" ]] && TLD=".home"
  [[ "$TLD" != .* ]] && TLD=".$TLD"

  echo ""
  read -rp "$(echo -e "  ${YELLOW}CA name (default: DashCaddy Local CA):${NC} ")" input
  [[ -n "$input" ]] && CA_NAME="$input"
}

# ============================================================================
# Dependency Installation
# ============================================================================

install_prereqs() {
  # Only install what's missing
  local needed=()
  command -v curl &>/dev/null || needed+=(curl)
  command -v jq   &>/dev/null || needed+=(jq)
  command -v dig  &>/dev/null || needed+=(dnsutils)
  # git only needed if using --source with a repo URL
  [[ -n "$DASHCADDY_REPO" ]] && ! command -v git &>/dev/null && needed+=(git)

  if [[ ${#needed[@]} -eq 0 ]]; then
    ok "Prerequisites already installed"
    return
  fi

  progress "Installing ${needed[*]}..." bash -c "
    case '$DISTRO_FAMILY' in
      debian) apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ${needed[*]} ;;
      rhel)   dnf install -y -q ${needed[*]} 2>/dev/null || yum install -y -q ${needed[*]} ;;
      arch)   pacman -Sy --noconfirm ${needed[*]} ;;
    esac
  " || warn "Some prerequisites may not have installed"
}

install_docker() {
  if $SKIP_DOCKER; then ok "Docker: skipped (--skip-docker)"; return; fi

  if command -v docker &>/dev/null && docker info &>/dev/null; then
    ok "Docker: $(docker --version | grep -oP 'Docker version [\d.]+')"
    return
  fi

  progress "Installing Docker (this is the slowest step)..." bash -c "
    case '$DISTRO_FAMILY' in
      debian)
        curl -fsSL https://get.docker.com | sh
        ;;
      rhel)
        if command -v dnf &>/dev/null; then
          dnf install -y -q dnf-plugins-core
          dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
          dnf install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin
        else
          yum install -y -q yum-utils
          yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
          yum install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin
        fi
        ;;
      arch)
        pacman -Sy --noconfirm docker docker-compose
        ;;
    esac
    systemctl enable docker
    systemctl start docker
  " || fatal "Docker installation failed. Install manually: https://docs.docker.com/engine/install/"

  if docker info &>/dev/null; then
    ok "Docker: $(docker --version | grep -oP 'Docker version [\d.]+')"
  else
    fatal "Docker installed but daemon not running. Try: systemctl start docker"
  fi
}

install_caddy() {
  if $SKIP_CADDY; then ok "Caddy: skipped (--skip-caddy)"; return; fi

  if command -v caddy &>/dev/null; then
    ok "Caddy: $(caddy version 2>/dev/null | head -1)"
    return
  fi

  progress "Installing Caddy..." bash -c "
    case '$DISTRO_FAMILY' in
      debian)
        apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https
        curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
        curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null
        apt-get update -qq
        apt-get install -y -qq caddy
        ;;
      rhel)
        if command -v dnf &>/dev/null; then
          dnf install -y -q 'dnf-command(copr)'
          dnf copr enable -y @caddy/caddy
          dnf install -y -q caddy
        else
          yum install -y -q yum-plugin-copr
          yum copr enable -y @caddy/caddy
          yum install -y -q caddy
        fi
        ;;
      arch)
        pacman -Sy --noconfirm caddy
        ;;
    esac
    # Stop default Caddy — we configure it ourselves
    systemctl stop caddy 2>/dev/null || true
  " || fatal "Caddy installation failed. Install manually: https://caddyserver.com/docs/install"

  ok "Caddy: $(caddy version 2>/dev/null | head -1)"
}

# ============================================================================
# Fix known OS issues automatically
# ============================================================================

fix_system_issues() {
  # systemd-resolved stub — breaks Docker DNS and Tailscale
  if [[ -L /etc/resolv.conf ]] && readlink /etc/resolv.conf | grep -q stub; then
    rm -f /etc/resolv.conf
    cat > /etc/resolv.conf <<'EOF'
# Set by DashCaddy installer (replaced systemd-resolved stub)
nameserver 1.1.1.1
nameserver 8.8.8.8
nameserver 1.0.0.1
EOF
    ok "Fixed systemd-resolved stub (replaced with public DNS)"
  fi

  # Ensure /etc/caddy exists
  mkdir -p /etc/caddy
}

# ============================================================================
# Directory & File Setup
# ============================================================================

create_directories() {
  mkdir -p "$INSTALL_DIR" "$DOCKER_DATA" "$SITES_DIR" "$API_DIR" "$DASHBOARD_DIR" "${DASHBOARD_DIR}/assets"
  mkdir -p /opt/dashcaddy/updates /opt/dashcaddy/scripts
  mkdir -p "${BACKUP_DIR}"
  ok "Directories created"
}

fetch_source() {
  local tmp_src="/tmp/dashcaddy-src-$$"

  if [[ -n "$SOURCE_PATH" ]]; then
    if [[ ! -d "$SOURCE_PATH" ]]; then
      fatal "Source path not found: $SOURCE_PATH"
    fi
    tmp_src="$SOURCE_PATH"
    ok "Using local source: $SOURCE_PATH"
  elif [[ -n "$DASHCADDY_REPO" ]]; then
    # Git clone mode (for development)
    progress "Cloning DashCaddy..." \
      git clone --depth 1 --branch "$GIT_BRANCH" "$DASHCADDY_REPO" "$tmp_src" \
      || fatal "Failed to clone DashCaddy. Check network."
  else
    # Download release tarball (default — no git needed)
    mkdir -p "$tmp_src"
    progress "Downloading DashCaddy v${DASHCADDY_VERSION}..." bash -c "
      curl -fsSL '${DASHCADDY_DOWNLOAD}' | tar xz -C '$tmp_src' --strip-components=1
    " || fatal "Failed to download DashCaddy. Check network or try --source with a local copy."
  fi

  # Find API source (handle different repo layouts)
  local api_src=""
  for try in "${tmp_src}/dashcaddy-api" "${tmp_src}/sites/dashcaddy-api" "${tmp_src}/api"; do
    [[ -d "$try" ]] && api_src="$try" && break
  done
  [[ -z "$api_src" ]] && fatal "Cannot find dashcaddy-api/ in source"

  # Find dashboard source
  local dash_src=""
  for try in "${tmp_src}/status" "${tmp_src}/sites/status" "${tmp_src}/dashboard"; do
    [[ -d "$try" ]] && dash_src="$try" && break
  done
  [[ -z "$dash_src" ]] && fatal "Cannot find dashboard source in source"

  # Deploy API files
  cp -f "${api_src}"/*.js "$API_DIR/" 2>/dev/null || true
  cp -f "${api_src}/package.json" "$API_DIR/"
  cp -f "${api_src}/package-lock.json" "$API_DIR/" 2>/dev/null || true
  cp -f "${api_src}/Dockerfile" "$API_DIR/"
  cp -f "${api_src}/openapi.yaml" "$API_DIR/" 2>/dev/null || true
  [[ -d "${api_src}/routes" ]] && cp -rf "${api_src}/routes" "$API_DIR/"
  [[ -d "${api_src}/src" ]] && cp -rf "${api_src}/src" "$API_DIR/"
  ok "API files deployed"

  # Deploy dashboard files
  cp -f "${dash_src}/index.html" "$DASHBOARD_DIR/"
  cp -f "${dash_src}/sw.js" "$DASHBOARD_DIR/" 2>/dev/null || true
  for dir in css js dist vendor assets; do
    [[ -d "${dash_src}/${dir}" ]] && cp -rf "${dash_src}/${dir}" "$DASHBOARD_DIR/"
  done
  ok "Dashboard files deployed"

  # Deploy updater scripts
  local scripts_src=""
  for try in "${api_src}/scripts" "${tmp_src}/scripts"; do
    [[ -d "$try" ]] && scripts_src="$try" && break
  done
  if [[ -n "$scripts_src" ]]; then
    cp -f "${scripts_src}/dashcaddy-update.sh" /opt/dashcaddy/scripts/ 2>/dev/null || true
    cp -f "${scripts_src}/dashcaddy-updater.path" /opt/dashcaddy/scripts/ 2>/dev/null || true
    cp -f "${scripts_src}/dashcaddy-updater.service" /opt/dashcaddy/scripts/ 2>/dev/null || true
    chmod +x /opt/dashcaddy/scripts/dashcaddy-update.sh 2>/dev/null || true
    ok "Updater scripts deployed"
  fi

  # Cleanup
  [[ -z "$SOURCE_PATH" ]] && rm -rf "$tmp_src"
}

create_seed_configs() {
  local config_type dashboard_host tz

  case "$DOMAIN_MODE" in
    public)     config_type="public";  dashboard_host="$DOMAIN" ;;
    custom-tld) config_type="homelab"; dashboard_host="dashcaddy${TLD}" ;;
    local)      config_type="local";   dashboard_host="${LAN_IP}:${LOCAL_PORT}" ;;
  esac

  tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo 'UTC')

  # Only create files that don't already exist (preserve on re-install)
  [[ -f "${INSTALL_DIR}/services.json" ]]         || echo '[]' > "${INSTALL_DIR}/services.json"
  [[ -f "${INSTALL_DIR}/dns-credentials.json" ]]   || echo '{}' > "${INSTALL_DIR}/dns-credentials.json"
  [[ -f "${INSTALL_DIR}/credentials.json" ]]       || echo '{}' > "${INSTALL_DIR}/credentials.json"
  [[ -f "${INSTALL_DIR}/notifications.json" ]]     || echo '[]' > "${INSTALL_DIR}/notifications.json"

  if [[ ! -f "${INSTALL_DIR}/.encryption-key" ]]; then
    openssl rand -hex 32 > "${INSTALL_DIR}/.encryption-key"
    chmod 600 "${INSTALL_DIR}/.encryption-key"
  fi

  # config.json — always regenerate (contains dashboard URL which may change)
  cat > "${INSTALL_DIR}/config.json" <<CFGEOF
{
  "setupComplete": true,
  "configurationType": "${config_type}",
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)",
  "timezone": "${tz}",
  "dashboardTitle": "DashCaddy",
  "dashboardHost": "${dashboard_host}",
  "tld": "${TLD}",
  "caName": "${CA_NAME}",
  "license": {
    "active": true,
    "tier": "community",
    "features": []
  }
}
CFGEOF

  ok "Config files ready"
}

# ============================================================================
# Caddyfile Generation
# ============================================================================

generate_caddyfile() {
  local cf="${INSTALL_DIR}/Caddyfile"

  # --- Shared snippets ---
  local snippets
  read -r -d '' snippets <<'SNIP' || true
(cors-preflight) {
  @preflight method OPTIONS
  header @preflight {
    Access-Control-Allow-Origin "*"
    Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
    Access-Control-Allow-Headers "*"
    Access-Control-Max-Age "600"
  }
  respond @preflight 204
}
(cors-allow) {
  header {
    Access-Control-Allow-Origin "*"
    Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
    Access-Control-Allow-Headers "*"
    Access-Control-Max-Age "600"
  }
}
SNIP

  local auth_snippet="(dashcaddy_auth) {
  forward_auth localhost:${API_PORT} {
    uri /api/v1/auth/gate/{args[0]}
    copy_headers Authorization X-Api-Key X-App-Cookie X-Emby-Token X-Plex-Token
  }
}"

  local site_body="  root * ${DASHBOARD_DIR}
  encode gzip

  handle /api/* {
    reverse_proxy localhost:${API_PORT}
  }

  handle {
    @notFile not file {path}
    rewrite @notFile /index.html
    file_server
  }"

  # --- Mode-specific Caddyfile ---
  case "$DOMAIN_MODE" in
    public)
      cat > "$cf" <<CEOF
# DashCaddy - Public Domain (Let's Encrypt)
{
  admin localhost:${CADDY_ADMIN_PORT}
$([ -n "$EMAIL" ] && echo "  email ${EMAIL}")
}

${snippets}

${auth_snippet}

${DOMAIN} {
${site_body}
}
CEOF
      ;;

    custom-tld)
      cat > "$cf" <<CEOF
# DashCaddy - Custom TLD (${TLD})
{
  admin localhost:${CADDY_ADMIN_PORT}
  pki {
    ca local {
      name "${CA_NAME}"
      root_cn "${CA_NAME} Root CA"
      intermediate_cn "${CA_NAME} Intermediate CA"
    }
  }
}

${snippets}

${auth_snippet}

dashcaddy${TLD} {
  tls internal
${site_body}
}
CEOF
      ;;

    local)
      cat > "$cf" <<CEOF
# DashCaddy - Local Mode
{
  admin localhost:${CADDY_ADMIN_PORT}
  auto_https off
}

${snippets}

:${LOCAL_PORT} {
${site_body}
}
CEOF
      ;;
  esac

  ok "Caddyfile generated"
}

# ============================================================================
# Docker Compose
# ============================================================================

# Parse size string like "10GB" or "1TB" to bytes
parse_size_to_bytes() {
  local size="$1"
  local value unit

  # Strip whitespace
  size=$(echo "$size" | tr -d ' ')

  # Extract numeric value and unit
  if [[ $size =~ ^([0-9.]+)([kmgtKMGT][bb]?|[bB]?)$ ]]; then
    value="${BASH_REMATCH[1]}"
    unit="${BASH_REMATCH[2]}"

    # Normalize unit to uppercase without 'B' suffix for simplicity
    unit=$(echo "$unit" | tr '[:lower:]' '[:upper:]')
    case "$unit" in
      K|KB)      echo $((value * 1024)) ;;
      M|MB)      echo $((value * 1024 * 1024)) ;;
      G|GB)      echo $((value * 1024 * 1024 * 1024)) ;;
      T|TB)      echo $((value * 1024 * 1024 * 1024 * 1024)) ;;
      *)         echo "$value" ;;
    esac
  else
    # Not recognized, treat as raw bytes
    echo "$size"
  fi
}

generate_docker_compose() {
  # Convert BACKUP_LIMIT to bytes if set (e.g., "10GB" -> 10737418240)
  local backup_limit_bytes=""
  if [[ -n "$BACKUP_LIMIT" ]]; then
    backup_limit_bytes=$(parse_size_to_bytes "$BACKUP_LIMIT")
  fi

  cat > "${API_DIR}/docker-compose.yml" <<DCEOF
services:
  dashcaddy-api:
    build: .
    container_name: ${CONTAINER_NAME}
    ports:
      - "${API_PORT}:${API_PORT}"
    volumes:
      - ${INSTALL_DIR}/Caddyfile:/caddyfile:rw
      - ${INSTALL_DIR}/services.json:/app/services.json:rw
      - ${INSTALL_DIR}/dns-credentials.json:/app/dns-credentials.json:rw
      - ${INSTALL_DIR}/config.json:/app/config.json:rw
      - ${INSTALL_DIR}/credentials.json:/app/credentials.json:rw
      - ${INSTALL_DIR}/.encryption-key:/app/.encryption-key:rw
      - ${INSTALL_DIR}/.license-secret:/app/.license-secret:ro
      - ${INSTALL_DIR}/totp-config.json:/app/totp-config.json:rw
      - ${INSTALL_DIR}/notifications.json:/app/notifications.json:rw
      - ${DASHBOARD_DIR}/assets:/app/assets:rw
      - ${DASHBOARD_DIR}:/app/dashboard:rw
      - /opt/dashcaddy/updates:/app/updates:rw
      - /var/run/docker.sock:/var/run/docker.sock
      - dashcaddy-backups:/app/backups
    environment:
      - CADDYFILE_PATH=/caddyfile
      - CADDY_ADMIN_URL=http://host.docker.internal:${CADDY_ADMIN_PORT}
      - ASSETS_PATH=/app/assets
      - CREDENTIALS_FILE=/app/credentials.json
      - CONFIG_FILE=/app/config.json
      - SERVICES_FILE=/app/services.json
      - DNS_CREDENTIALS_FILE=/app/dns-credentials.json
      - HOST_LAN_IP=${LAN_IP}
      - NODE_ENV=production
      - DASHCADDY_UPDATE_ENABLED=true
      - DASHCADDY_UPDATE_URL=https://get.dashcaddy.net/release
      - DASHCADDY_MIRROR_URL=https://get2.dashcaddy.net/release
      - DASHCADDY_UPDATES_DIR=/app/updates
      - DASHCADDY_HOST_UPDATES_DIR=/opt/dashcaddy/updates
      - DASHCADDY_API_SOURCE_DIR=${API_DIR}
      - DASHCADDY_FRONTEND_DIR=/app/dashboard
      - BACKUP_DIR=/app/backups
      - BACKUP_MAX_STORAGE_BYTES=${backup_limit_bytes:-0}
      - BACKUP_CONFIG_FILE=/app/backup-config.json
      - BACKUP_HISTORY_FILE=/app/backup-history.json
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  dashcaddy-backups:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${BACKUP_DIR}
DCEOF

  ok "docker-compose.yml generated"
}

# ============================================================================
# Auto-Updater Service
# ============================================================================

install_updater_service() {
  local scripts_dir="/opt/dashcaddy/scripts"

  if [[ ! -f "${scripts_dir}/dashcaddy-update.sh" ]]; then
    warn "Updater script not found — skipping auto-update service"
    return
  fi

  # Install systemd units
  if [[ -f "${scripts_dir}/dashcaddy-updater.path" ]]; then
    cp -f "${scripts_dir}/dashcaddy-updater.path" /etc/systemd/system/
  else
    cat > /etc/systemd/system/dashcaddy-updater.path <<'PATHEOF'
[Unit]
Description=Watch for DashCaddy update trigger
[Path]
PathChanged=/opt/dashcaddy/updates/trigger.json
[Install]
WantedBy=multi-user.target
PATHEOF
  fi

  if [[ -f "${scripts_dir}/dashcaddy-updater.service" ]]; then
    cp -f "${scripts_dir}/dashcaddy-updater.service" /etc/systemd/system/
  else
    cat > /etc/systemd/system/dashcaddy-updater.service <<'SVCEOF'
[Unit]
Description=DashCaddy auto-update handler
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/opt/dashcaddy/scripts/dashcaddy-update.sh
TimeoutStartSec=300
StandardOutput=journal
StandardError=journal
SyslogIdentifier=dashcaddy-update
SVCEOF
  fi

  systemctl daemon-reload
  systemctl enable dashcaddy-updater.path >/dev/null 2>&1
  systemctl start dashcaddy-updater.path >/dev/null 2>&1
  ok "Auto-updater service installed"
}

# ============================================================================
# Build & Launch
# ============================================================================

build_and_start() {
  # Remove old container if exists
  docker rm -f "$CONTAINER_NAME" 2>/dev/null || true

  cd "$API_DIR"

  progress "Building container image (30-60s)..." \
    docker compose build --quiet \
    || progress "Building container image (fallback)..." \
      docker-compose build --quiet \
    || fatal "Docker build failed. Check: docker info"

  progress "Starting container..." \
    docker compose up -d \
    || docker-compose up -d 2>/dev/null \
    || fatal "Failed to start container"

  # Wait for healthy
  local i=0
  printf "${CYAN}  ▸${NC} Waiting for API health check "
  while (( i < 30 )); do
    if curl -fsSL --max-time 2 "http://localhost:${API_PORT}/health" &>/dev/null; then
      echo -e "${GREEN}healthy${NC}"
      return
    fi
    printf "."
    sleep 1
    i=$((i + 1))
  done
  echo -e "${YELLOW}timeout${NC}"
  warn "API may still be starting. Check: docker logs ${CONTAINER_NAME}"
}

start_caddy() {
  # Symlink our Caddyfile to where Caddy's systemd unit expects it
  if [[ -f /etc/caddy/Caddyfile && ! -L /etc/caddy/Caddyfile ]]; then
    mv /etc/caddy/Caddyfile /etc/caddy/Caddyfile.original 2>/dev/null || true
  fi
  ln -sf "${INSTALL_DIR}/Caddyfile" /etc/caddy/Caddyfile

  systemctl enable caddy >/dev/null 2>&1
  systemctl restart caddy 2>/dev/null

  sleep 2
  if systemctl is-active --quiet caddy; then
    ok "Caddy is running"
  else
    warn "Caddy may have issues. Check: systemctl status caddy"
  fi
}

# ============================================================================
# Firewall
# ============================================================================

open_firewall() {
  if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "active"; then
    case "$DOMAIN_MODE" in
      public)
        ufw allow 80/tcp >/dev/null 2>&1; ufw allow 443/tcp >/dev/null 2>&1
        ok "Firewall: opened 80, 443 (UFW)" ;;
      local)
        ufw allow "${LOCAL_PORT}"/tcp >/dev/null 2>&1
        ok "Firewall: opened ${LOCAL_PORT} (UFW)" ;;
    esac
  elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld 2>/dev/null; then
    case "$DOMAIN_MODE" in
      public)
        firewall-cmd --permanent --add-service=http --add-service=https >/dev/null 2>&1
        firewall-cmd --reload >/dev/null 2>&1
        ok "Firewall: opened 80, 443 (firewalld)" ;;
      local)
        firewall-cmd --permanent --add-port="${LOCAL_PORT}"/tcp >/dev/null 2>&1
        firewall-cmd --reload >/dev/null 2>&1
        ok "Firewall: opened ${LOCAL_PORT} (firewalld)" ;;
    esac
  fi
}

# ============================================================================
# Uninstall
# ============================================================================

do_uninstall() {
  echo -e "\n${BOLD}  Uninstalling DashCaddy${NC}\n"

  echo "  This will:"
  echo "    - Stop and remove the dashcaddy-api container"
  echo "    - Remove DashCaddy files from ${INSTALL_DIR}/"
  if $KEEP_CONFIG; then
    echo "    - KEEP your config files (--keep-config)"
  fi
  echo "    - NOT remove Docker or Caddy"
  echo ""

  read -rp "$(echo -e "  ${YELLOW}Continue? [y/N]:${NC} ")" -n1 answer
  echo ""
  [[ "$answer" =~ ^[Yy]$ ]] || { echo "  Cancelled."; exit 0; }

  docker rm -f "$CONTAINER_NAME" 2>/dev/null && ok "Container removed" || true

  # Stop and remove updater service
  systemctl stop dashcaddy-updater.path 2>/dev/null || true
  systemctl disable dashcaddy-updater.path 2>/dev/null || true
  rm -f /etc/systemd/system/dashcaddy-updater.path /etc/systemd/system/dashcaddy-updater.service
  systemctl daemon-reload 2>/dev/null || true
  rm -rf /opt/dashcaddy && ok "Updater service removed" || true

  if [[ -L /etc/caddy/Caddyfile ]] && readlink /etc/caddy/Caddyfile | grep -q dashcaddy; then
    rm -f /etc/caddy/Caddyfile
    [[ -f /etc/caddy/Caddyfile.original ]] && mv /etc/caddy/Caddyfile.original /etc/caddy/Caddyfile
    systemctl restart caddy 2>/dev/null || true
    ok "Caddy config restored"
  fi

  if $KEEP_CONFIG; then
    rm -rf "$API_DIR" "$DASHBOARD_DIR"
    ok "App files removed, config preserved in ${INSTALL_DIR}/"
  else
    rm -rf "$INSTALL_DIR"
    ok "All files removed from ${INSTALL_DIR}/"
  fi

  echo -e "\n${GREEN}  DashCaddy uninstalled.${NC}\n"
  exit 0
}

# ============================================================================
# Argument Parsing
# ============================================================================

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      quick)        QUICK=true; DOMAIN_MODE="local"; shift ;;
      --domain)     DOMAIN_MODE="public"; DOMAIN="${2:-}"; shift; shift ;;
      --email)      EMAIL="${2:-}"; shift; shift ;;
      --tld)        DOMAIN_MODE="custom-tld"; TLD="${2:-}"; shift; shift ;;
      --ca-name)    CA_NAME="${2:-}"; shift; shift ;;
      --local)      DOMAIN_MODE="local"; shift ;;
      --port)       LOCAL_PORT="${2:-8080}"; shift; shift ;;
      --api-port)   API_PORT="${2:-3001}"; shift; shift ;;
      --source)     SOURCE_PATH="${2:-}"; shift; shift ;;
      --branch)     GIT_BRANCH="${2:-main}"; shift; shift ;;
      --skip-docker) SKIP_DOCKER=true; shift ;;
      --skip-caddy)  SKIP_CADDY=true; shift ;;
      --uninstall)   UNINSTALL=true; shift ;;
      --keep-config) KEEP_CONFIG=true; shift ;;
      --backup-dir)  BACKUP_DIR="${2:-}"; shift; shift ;;
      --backup-limit) BACKUP_LIMIT="${2:-}"; shift; shift ;;
      --yes|-y)      AUTO_YES=true; shift ;;
      --help|-h)     print_help; exit 0 ;;
      *) warn "Unknown option: $1 (ignored)"; shift ;;
    esac
  done

  # Normalize TLD
  if [[ -n "$TLD" && "$TLD" != .* ]]; then TLD=".$TLD"; fi
}

print_help() {
  cat <<'HELP'

  DashCaddy Linux Installer

  QUICK INSTALL (zero typing):
    curl -fsSL https://get.dashcaddy.net | bash -s -- quick

  WITH DOMAIN:
    curl -fsSL https://get.dashcaddy.net | bash -s -- --domain my.example.com

  INTERACTIVE:
    curl -fsSL https://get.dashcaddy.net | bash

  OPTIONS:
    quick              Instant local install, zero questions
    --domain DOMAIN    Public domain (Let's Encrypt TLS)
    --email EMAIL      Let's Encrypt email (optional)
    --tld TLD          Custom TLD, internal CA (e.g., .home)
    --local            Local mode (http://IP:port)
    --port PORT        Port for local mode (default: 8080)
    --source PATH      Use local source files
    --skip-docker      Already have Docker
    --skip-caddy       Already have Caddy
    --backup-dir PATH  Backup directory (default: /etc/dashcaddy/backups)
    --backup-limit SIZE Storage limit for backups (e.g., 10GB, 1TB)
    --uninstall        Remove DashCaddy
    --keep-config      Keep configs during uninstall
    --yes              Skip confirmations

HELP
}

# ============================================================================
# Banner & Success
# ============================================================================

print_banner() {
  echo ""
  echo -e "${BOLD}${BLUE}  ╔══════════════════════════════════════════════╗${NC}"
  echo -e "${BOLD}${BLUE}  ║${NC}${BOLD}          DashCaddy Installer v${DASHCADDY_VERSION}          ${BLUE}║${NC}"
  echo -e "${BOLD}${BLUE}  ╚══════════════════════════════════════════════╝${NC}"
  echo ""
}

print_success() {
  local total_time=$1
  local url

  case "$DOMAIN_MODE" in
    public)     url="https://${DOMAIN}" ;;
    custom-tld) url="https://dashcaddy${TLD}" ;;
    local)      url="http://${PUBLIC_IP}:${LOCAL_PORT}" ;;
  esac

  # Also show LAN URL for local mode
  local lan_url=""
  if [[ "$DOMAIN_MODE" == "local" && "$LAN_IP" != "$PUBLIC_IP" ]]; then
    lan_url="http://${LAN_IP}:${LOCAL_PORT}"
  fi

  echo ""
  echo -e "${GREEN}${BOLD}  ┌──────────────────────────────────────────────┐${NC}"
  echo -e "${GREEN}${BOLD}  │           Installation Complete!              │${NC}"
  echo -e "${GREEN}${BOLD}  └──────────────────────────────────────────────┘${NC}"
  echo ""
  echo -e "  ${BOLD}Open in browser:${NC}  ${url}"
  [[ -n "$lan_url" ]] && echo -e "  ${BOLD}LAN access:${NC}       ${lan_url}"
  echo ""
  echo -e "  ${DIM}Config: ${INSTALL_DIR}/  |  Logs: docker logs dashcaddy-api${NC}"
  echo -e "  ${DIM}Installed in: ${total_time}${NC}"

  if [[ "$DOMAIN_MODE" == "public" ]]; then
    echo ""
    echo -e "  ${CYAN}TLS will auto-provision on first visit.${NC}"
    echo -e "  ${CYAN}Ensure DNS for ${DOMAIN} → ${PUBLIC_IP}${NC}"
  fi

  echo ""
  echo -e "  ${BOLD}Everything is managed from the dashboard — no CLI needed.${NC}"
  echo -e "  ${DIM}Deploy apps, manage Docker containers, edit Caddy configs,${NC}"
  echo -e "  ${DIM}and monitor services all from the web UI.${NC}"
  echo ""
}

# ============================================================================
# Main
# ============================================================================

main() {
  local start_time
  start_time=$(date +%s)

  parse_args "$@"

  # Handle uninstall
  if $UNINSTALL; then do_uninstall; fi

  print_banner

  # ---- Step 1: System check ----
  step "Checking system"
  detect_system
  fix_system_issues

  # ---- Step 2: Configuration ----
  if $QUICK; then
    ok "Quick mode: http://${PUBLIC_IP}:${LOCAL_PORT}"
  else
    step "Configuration"
    interactive_setup
  fi

  # Show what we're doing
  echo ""
  case "$DOMAIN_MODE" in
    public)     log "Installing with domain: ${DOMAIN}" ;;
    custom-tld) log "Installing with TLD: ${TLD}" ;;
    local)      log "Installing in local mode (port ${LOCAL_PORT})" ;;
  esac

  # ---- Step 3: Dependencies ----
  step "Installing dependencies"
  install_prereqs
  install_docker
  install_caddy

  # ---- Step 4: Deploy files ----
  step "Deploying DashCaddy"
  create_directories
  fetch_source
  create_seed_configs

  # ---- Step 5: Configuration ----
  step "Generating configuration"
  generate_caddyfile
  generate_docker_compose
  open_firewall

  # ---- Step 6: Build & start ----
  step "Building & starting services"
  build_and_start
  install_updater_service

  # ---- Step 7: Start Caddy ----
  step "Starting web server"
  start_caddy

  print_success "$(elapsed "$start_time")"
}

main "$@"
