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

DEFAULT_SERVER="apache"
DEFAULT_MODE="static"
DEFAULT_WEBROOT_BASE="/var/www"
DEFAULT_ACME_WEBROOT="/var/www/_letsencrypt"
DEFAULT_GODADDY_DOMAIN="pixellar.pe"
DEFAULT_GODADDY_SEARCH_DIRS="/etc/ssl,/etc/pki,/etc/apache2,/etc/httpd"
DEFAULT_NGINX_CONF_DIR="/etc/nginx/conf.d"
DEFAULT_APACHE_PATH="/etc/apache2"
DEFAULT_LE_LIVE_DIR="/etc/letsencrypt/live"
DEFAULT_MKCERT_DIR="/etc/ssl/mkcert"
DEFAULT_HOSTS_FILE="/etc/hosts"
DEFAULT_LOCAL_HOSTS_IP="${SSL_ADMIN_LOCAL_IP:-127.0.0.1}"
HOSTS_MANAGED_TAG="# ssl-admin managed"
LAST_CREATED_FQDN=""
IN_MENU_MODE=0
UI_CANCEL_REQUESTED=0
UI_ALT_SCREEN=0
UI_STTY_SAVED=""
UI_STTY_RAW=0
PIXELLAR_BANNER_OGRE=$'       _          _ _            \n _ __ (_)_  _____| | | __ _ _ __ \n| \'_ \\| \\ \\/ / _ \\ | |/ _` | \'__|\n| |_) | |>  <  __/ | | (_| | |   \n| .__/|_/_/\\_\\___|_|_|\\__,_|_|   \n|_|                              '
ORANGE='\033[38;5;208m'
BOLD='\033[1m'
RESET='\033[0m'
SSL_OK_COLOR='\033[38;5;82m'
SSL_EXPIRING_COLOR='\033[38;5;214m'
SSL_EXPIRED_COLOR='\033[38;5;196m'
SSL_NO_CERT_COLOR='\033[38;5;244m'
SSL_INVALID_COLOR='\033[38;5;160m'
SSL_UNKNOWN_COLOR='\033[38;5;75m'

log() { printf "[INFO] %s\n" "$*"; }
warn() { printf "[WARN] %s\n" "$*"; }
err() { printf "[ERROR] %s\n" "$*" >&2; }
die() {
  err "$*"
  if [[ "${IN_MENU_MODE:-0}" -eq 1 ]]; then
    return 1
  fi
  exit 1
}

on_ctrl_c() {
  UI_CANCEL_REQUESTED=1
  printf "\n"
}

restore_terminal_state() {
  if [[ "${UI_STTY_RAW:-0}" -eq 1 && -n "${UI_STTY_SAVED:-}" ]]; then
    stty "$UI_STTY_SAVED" 2>/dev/null || true
    UI_STTY_RAW=0
  fi

  if [[ "${UI_ALT_SCREEN:-0}" -eq 1 ]]; then
    UI_ALT_SCREEN=0
  fi
}

on_exit() {
  restore_terminal_state
}

trap 'on_ctrl_c' INT
trap 'on_exit' EXIT

need_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}

require_root() {
  [[ "${EUID}" -eq 0 ]] || die "Run as root (use sudo)."
}

ensure_sudo_session() {
  local cmd="${1:-menu}"

  if [[ "${SSL_ADMIN_SKIP_SUDO_CHECK:-0}" == "1" ]]; then
    return 0
  fi

  case "$cmd" in
    help|-h|--help|status|list|find-godaddy-certs|doctor)
      return 0
      ;;
  esac

  if [[ "${EUID}" -eq 0 ]]; then
    return 0
  fi

  command -v sudo >/dev/null 2>&1 || die "sudo no esta instalado. Ejecuta como root."

  echo
  echo "[INFO] Se requieren permisos sudo para esta operacion."
  sudo -v || die "No se pudo validar sudo."

  # Re-launch as root and keep those privileges for the whole session.
  if [[ "$#" -eq 0 ]]; then
    exec sudo --preserve-env=TERM,COLORTERM,LANG,LC_ALL -- "${BASH:-bash}" "$0"
  fi
  exec sudo --preserve-env=TERM,COLORTERM,LANG,LC_ALL -- "${BASH:-bash}" "$0" "$@"
}

is_valid_fqdn() {
  local fqdn="$1"
  [[ "$fqdn" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]]
}

trim_spaces() {
  local s="$1"
  s="${s#"${s%%[![:space:]]*}"}"
  s="${s%"${s##*[![:space:]]}"}"
  echo "$s"
}

is_valid_email_basic() {
  local email="$1"
  [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]]
}

is_valid_http_url() {
  local url="$1"
  [[ "$url" =~ ^https?://[^[:space:]/:?#]+(:[0-9]{1,5})?(/[^[:space:]]*)?$ ]]
}

is_valid_document_root_path() {
  local path="$1"
  [[ "$path" == /* ]] || return 1
  [[ "$path" != "/" ]] || return 1
  [[ "$path" != *[[:space:]]* ]] || return 1
  [[ "$path" != *$'\n'* ]] || return 1
  [[ "$path" != *$'\r'* ]] || return 1
  return 0
}

normalize_lower() {
  echo "$1" | tr '[:upper:]' '[:lower:]'
}

json_escape() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  s="${s//$'\n'/\\n}"
  s="${s//$'\r'/\\r}"
  s="${s//$'\t'/\\t}"
  printf "%s" "$s"
}

clean_conf_value() {
  local v="$1"
  v="${v%;}"
  if [[ "${v:0:1}" == "\"" && "${v: -1}" == "\"" ]]; then
    v="${v:1:${#v}-2}"
  fi
  echo "$v"
}

is_vhost_candidate_file() {
  local file="$1"
  local base
  [[ -f "$file" ]] || return 1
  base="$(basename "$file")"
  case "$base" in
    *.bak|*.bak.*|*.disabled.*|*.tmp|*.temp|*.save|*.orig|*.rej|*.swp|*.dpkg-*|*.rpmnew|*.rpmsave|*~)
      return 1
      ;;
  esac
  return 0
}

ssl_state_color_code() {
  local state="$1"
  case "$state" in
    OK) echo "$SSL_OK_COLOR" ;;
    EXPIRING) echo "$SSL_EXPIRING_COLOR" ;;
    EXPIRED) echo "$SSL_EXPIRED_COLOR" ;;
    NO_CERT) echo "$SSL_NO_CERT_COLOR" ;;
    INVALID) echo "$SSL_INVALID_COLOR" ;;
    UNKNOWN) echo "$SSL_UNKNOWN_COLOR" ;;
    *) echo "$RESET" ;;
  esac
}

ssl_state_colored_paren() {
  local state="$1"
  if [[ -z "$state" || "$state" == "-" ]]; then
    echo "-"
    return 0
  fi
  local c
  c="$(ssl_state_color_code "$state")"
  printf "%b(%s)%b" "$c" "$state" "$RESET"
}

is_private_or_loopback_ipv4() {
  local ip="$1"
  [[ "$ip" =~ ^127\. ]] && return 0
  [[ "$ip" =~ ^10\. ]] && return 0
  [[ "$ip" =~ ^192\.168\. ]] && return 0
  if [[ "$ip" =~ ^172\.([0-9]+)\. ]]; then
    local second="${BASH_REMATCH[1]}"
    if (( second >= 16 && second <= 31 )); then
      return 0
    fi
  fi
  return 1
}

resolve_fqdn_ipv4() {
  local fqdn="$1"
  local ip=""

  if command -v getent >/dev/null 2>&1; then
    ip="$(getent ahostsv4 "$fqdn" 2>/dev/null | awk 'NR==1{print $1}')"
  fi

  if [[ -z "$ip" ]] && command -v dscacheutil >/dev/null 2>&1; then
    ip="$(dscacheutil -q host -a name "$fqdn" 2>/dev/null | awk '/^ip_address:/{print $2; exit}')"
  fi

  if [[ -z "$ip" ]] && command -v host >/dev/null 2>&1; then
    ip="$(host "$fqdn" 2>/dev/null | awk '/ has address /{print $4; exit}')"
  fi

  if [[ -z "$ip" ]] && command -v dig >/dev/null 2>&1; then
    ip="$(dig +short A "$fqdn" 2>/dev/null | awk 'NF{print; exit}')"
  fi

  echo "$ip"
}

hosts_managed_count_for_fqdn() {
  local fqdn="$1"
  local hosts_file="${2:-$DEFAULT_HOSTS_FILE}"
  [[ -f "$hosts_file" ]] || { echo "0"; return 0; }
  awk -v fq="$fqdn" -v tag="$HOSTS_MANAGED_TAG" '
    index($0, tag) > 0 {
      for (i=1; i<=NF; i++) {
        if ($i == fq) {
          c++
          break
        }
      }
    }
    END { print c+0 }
  ' "$hosts_file"
}

remove_managed_hosts_entry() {
  local fqdn="$1"
  local hosts_file="${2:-$DEFAULT_HOSTS_FILE}"
  [[ -f "$hosts_file" ]] || return 0

  local tmp_file
  tmp_file="$(mktemp "${TMPDIR:-/tmp}/ssl-admin-hosts.XXXXXX")" || return 1

  awk -v fq="$fqdn" -v tag="$HOSTS_MANAGED_TAG" '
    {
      if (index($0, tag) > 0) {
        for (i=1; i<=NF; i++) {
          if ($i == fq) {
            next
          }
        }
      }
      print
    }
  ' "$hosts_file" > "$tmp_file"

  cat "$tmp_file" > "$hosts_file"
  rm -f "$tmp_file"
}

ensure_managed_hosts_entry() {
  local fqdn="$1"
  local ip="${2:-$DEFAULT_LOCAL_HOSTS_IP}"
  local hosts_file="${3:-$DEFAULT_HOSTS_FILE}"

  remove_managed_hosts_entry "$fqdn" "$hosts_file"

  if [[ -s "$hosts_file" ]]; then
    local last_char
    last_char="$(tail -c 1 "$hosts_file" 2>/dev/null || true)"
    if [[ "$last_char" != $'\n' ]]; then
      printf "\n" >> "$hosts_file"
    fi
  fi
  printf "%s\t%s\t%s\n" "$ip" "$fqdn" "$HOSTS_MANAGED_TAG" >> "$hosts_file"
  log "Hosts updated: ${fqdn} -> ${ip} (${hosts_file})"
}

trim_trailing_slash() {
  local in="$1"
  while [[ "$in" == */ ]]; do
    in="${in%/}"
  done
  echo "$in"
}

ask_text() {
  local __var="$1"
  local prompt="$2"
  local default="${3:-}"
  local answer=""
  local key=""
  local prompt_label=""

  if [[ -n "$default" ]]; then
    prompt_label="$prompt [$default]: "
  else
    prompt_label="$prompt: "
  fi

  printf "%s" "$prompt_label"
  while true; do
    if [[ "$UI_CANCEL_REQUESTED" -eq 1 ]]; then
      UI_CANCEL_REQUESTED=0
      printf "\n"
      return 130
    fi

    if ! IFS= read -rsn1 -t 1 key; then
      continue
    fi

    if [[ "$UI_CANCEL_REQUESTED" -eq 1 ]]; then
      UI_CANCEL_REQUESTED=0
      printf "\n"
      return 130
    fi

    case "$key" in
      ""|$'\n'|$'\r')
        break
        ;;
      $'\x03')
        printf "\n"
        return 130
        ;;
      $'\x18')
        printf "\n"
        exit 0
        ;;
      $'\x7f'|$'\b')
        if [[ -n "$answer" ]]; then
          answer="${answer%?}"
          printf "\b \b"
        fi
        ;;
      $'\x1b')
        printf "\n"
        return 130
        ;;
      *)
        if [[ "$key" =~ [[:print:]] ]]; then
          answer+="$key"
          printf "%s" "$key"
        fi
        ;;
    esac
  done
  printf "\n"

  if [[ -z "$answer" && -n "$default" ]]; then
    answer="$default"
  fi

  printf -v "$__var" "%s" "$answer"
}

ask_yes_no() {
  local prompt="$1"
  local default="${2:-no}"
  local ans=""

  while true; do
    if [[ "$default" == "yes" ]]; then
      ask_text ans "$prompt [Y/n]" "yes" || return $?
    else
      ask_text ans "$prompt [y/N]" "no" || return $?
    fi

    ans="$(normalize_lower "$ans")"
    case "$ans" in
      y|yes|s|si) return 0 ;;
      n|no) return 1 ;;
      *) warn "Please answer yes or no." ;;
    esac
  done
}

ask_choice() {
  local __var="$1"
  local prompt="$2"
  local default="$3"
  shift 3
  local options=("$@")
  local ans=""

  while true; do
    ask_text ans "$prompt (${options[*]})" "$default"
    local rc=$?
    if [[ "$rc" -ne 0 ]]; then
      return "$rc"
    fi
    ans="$(normalize_lower "$ans")"
    local opt
    for opt in "${options[@]}"; do
      if [[ "$ans" == "$opt" ]]; then
        printf -v "$__var" "%s" "$ans"
        return 0
      fi
    done
    warn "Invalid option: $ans"
  done
}

ui_enter_alt_screen() {
  if [[ -t 0 && "${UI_STTY_RAW:-0}" -eq 0 ]]; then
    UI_STTY_SAVED="$(stty -g 2>/dev/null || true)"
    if [[ -n "$UI_STTY_SAVED" ]]; then
      # Disable ISIG so Ctrl+C is handled as keypress in the TUI.
      stty -echo -icanon -isig min 1 time 0 2>/dev/null || true
      UI_STTY_RAW=1
    fi
  fi

  # Use normal terminal buffer so scrollback remains available.
  UI_ALT_SCREEN=0
}

ui_clear() {
  printf "\033[2J\033[H"
}

ui_key_help_main() {
  echo "[ N: Nvo.Dominio | U: Act.SSL | ENTER: detalle | BACKSPACE: borrar ]"
  echo
}

ui_key_help_create() {
  echo "[ ESC/Ctrl+C: Regresar ]"
}

ui_key_help_detail() {
  echo "[ ENTER/ESC: regresar ]"
  echo
}

get_os_pretty_name() {
  if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    if [[ -n "${PRETTY_NAME:-}" ]]; then
      echo "$PRETTY_NAME"
      return 0
    fi
    if [[ -n "${NAME:-}" ]]; then
      echo "$NAME"
      return 0
    fi
  fi
  uname -srm
}

get_apache_active_root() {
  local info cmd root conf
  info="$(get_apache_runtime_info || true)"
  if [[ -n "$info" ]]; then
    IFS='|' read -r cmd root conf <<< "$info"
    if [[ -n "$conf" ]]; then
      local conf_dir
      conf_dir="$(dirname "$conf")"
      if [[ -n "$conf_dir" ]]; then
        echo "$conf_dir"
        return 0
      fi
    fi
    if [[ -n "$root" ]]; then
      echo "$root"
      return 0
    fi
  fi

  local apache_dir
  apache_dir="$(detect_apache_conf_dir)"
  if [[ "$apache_dir" == */vhosts ]]; then
    echo "${apache_dir%/vhosts}"
  elif [[ "$apache_dir" == */sites-available ]]; then
    echo "${apache_dir%/sites-available}"
  elif [[ "$apache_dir" == */conf.d ]]; then
    echo "${apache_dir%/conf.d}"
  else
    echo "$apache_dir"
  fi
}

ui_page_begin_with_header() {
  ui_clear
  printf "%b%s%b\n" "$ORANGE" "$PIXELLAR_BANNER_OGRE" "$RESET"
  #echo "ssl-admin v2"
  printf ">>>>>>>>>>>>>>>>>>>>> SSL ADMIN |\n\n"
  printf "${BOLD}OS:${RESET}      %s\n" "$(get_os_pretty_name)"
  printf "${BOLD}APACHE:${RESET}  %s\n" "$(get_apache_active_root)"
  echo
}

render_create_wizard_screen() {
  local cert_profile="$1"
  local fqdn="$2"
  local site_type="$3"
  local mode="$4"
  local document_root="$5"
  local upstream="$6"
  local email="$7"
  local form_error="${8:-}"

  ui_page_begin_with_header
  echo "------------------------------------------"
  printf "🏠 > Nuevo\n"
  echo "------------------------------------------"
  echo
  ui_key_help_create
  echo
  if [[ -n "$form_error" ]]; then
    printf "[!] %s\n" "$form_error"
  fi
  echo
}

ui_select_horizontal_inline() {
  local __result_var="$1"
  local prompt="$2"
  local default="$3"
  shift 3
  local options=("$@")
  local total="${#options[@]}"
  local cursor=0
  local i key

  (( total > 0 )) || return 130

  for ((i=0; i<total; i++)); do
    if [[ "${options[$i]}" == "$default" ]]; then
      cursor="$i"
      break
    fi
  done

  ui_drain_input
  while true; do
    printf "\r\033[2K%s: " "$prompt"
    for ((i=0; i<total; i++)); do
      if (( i == cursor )); then
        printf "%b[%s]%b" "$ORANGE" "${options[$i]}" "$RESET"
      else
        printf " %s " "${options[$i]}"
      fi
    done

    key="$(ui_read_key || true)"
    case "$key" in
      left|up)
        cursor=$(( (cursor - 1 + total) % total ))
        ;;
      right|down)
        cursor=$(( (cursor + 1) % total ))
        ;;
      enter)
        printf "\n"
        printf -v "$__result_var" "%s" "${options[$cursor]}"
        return 0
        ;;
      esc)
        printf "\n"
        return 130
        ;;
      ctrl_c)
        printf "\n"
        return 130
        ;;
      ctrl_x)
        printf "\n"
        exit 0
        ;;
      *)
        ;;
    esac
  done
}

ui_clear_prev_lines() {
  local n="${1:-0}"
  local i
  (( n > 0 )) || return 0
  for ((i=0; i<n; i++)); do
    printf "\033[1A\033[2K\r"
  done
}

ui_drain_input() {
  local _k
  while true; do
    if IFS= read -rsn1 -t 0 _k 2>/dev/null; then
      :
    else
      break
    fi
  done
}

ui_run_with_spinner() {
  local message="$1"
  shift
  local tmp_out=""
  local pid=""
  local rc=0
  local i=0
  local frames=('|' '/' '-' '\')

  tmp_out="$(mktemp "${TMPDIR:-/tmp}/ssl-admin-spinner.XXXXXX")" || {
    echo "$message"
    "$@"
    return $?
  }

  ("$@") >"$tmp_out" 2>&1 &
  pid="$!"

  while kill -0 "$pid" 2>/dev/null; do
    printf "\r\033[2K%s %s" "$message" "${frames[$i]}"
    i=$(( (i + 1) % 4 ))
    sleep 0.1
  done

  wait "$pid"
  rc=$?
  printf "\r\033[2K%s listo.\n" "$message"

  cat "$tmp_out"
  rm -f "$tmp_out"
  return "$rc"
}

ui_read_key() {
  local key rest ch

  while true; do
    if [[ "$UI_CANCEL_REQUESTED" -eq 1 ]]; then
      UI_CANCEL_REQUESTED=0
      echo "ctrl_c"
      return 0
    fi
    if IFS= read -rsn1 -t 1 key; then
      break
    fi
  done

  if [[ "$UI_CANCEL_REQUESTED" -eq 1 ]]; then
    UI_CANCEL_REQUESTED=0
    echo "ctrl_c"
    return 0
  fi

  if [[ -z "$key" || "$key" == $'\n' || "$key" == $'\r' ]]; then
    echo "enter"
    return 0
  fi

  case "$key" in
    $'\x1b')
      rest=""
      if ! ui_try_read_byte ch "${UI_ESC_WAIT_MS:-300}"; then
        echo "esc"
        return 0
      fi
      rest+="$ch"

      if [[ "$ch" == "[" || "$ch" == "O" ]]; then
        if ! ui_try_read_byte ch "${UI_ESC_WAIT_MS:-300}"; then
          echo "esc"
          return 0
        fi
        rest+="$ch"

        # Typical arrows: ESC [A / ESC OA
        case "$ch" in
          A) echo "up"; return 0 ;;
          B) echo "down"; return 0 ;;
          C) echo "right"; return 0 ;;
          D) echo "left"; return 0 ;;
        esac

        # Variant observed in some terminals: ESC [[A / ESC [[B
        if [[ "$ch" == "[" ]]; then
          if ui_try_read_byte ch "${UI_ESC_WAIT_MS:-300}"; then
            rest+="$ch"
            case "$ch" in
              A) echo "up"; return 0 ;;
              B) echo "down"; return 0 ;;
              C) echo "right"; return 0 ;;
              D) echo "left"; return 0 ;;
            esac
          fi
        fi

        # Extended CSI: ESC [1;5A, ESC [200~, etc.
        # Read until final byte (letter/~) or max length.
        local n=0
        while (( n < 8 )); do
          if ! ui_try_read_byte ch "${UI_ESC_WAIT_MS:-300}"; then
            break
          fi
          rest+="$ch"
          case "$ch" in
            A) echo "up"; return 0 ;;
            B) echo "down"; return 0 ;;
            C) echo "right"; return 0 ;;
            D) echo "left"; return 0 ;;
            "~"|[A-Za-z]) break ;;
          esac
          n=$((n + 1))
        done
      fi

      if [[ "$rest" == "[A" || "$rest" == "OA" || "$rest" == "[[A" ]]; then
        echo "up"
      elif [[ "$rest" == "[B" || "$rest" == "OB" || "$rest" == "[[B" ]]; then
        echo "down"
      elif [[ "$rest" == "[C" || "$rest" == "OC" || "$rest" == "[[C" ]]; then
        echo "right"
      elif [[ "$rest" == "[D" || "$rest" == "OD" || "$rest" == "[[D" ]]; then
        echo "left"
      else
        echo "esc"
      fi
      ;;
    " ")
      echo "space"
      ;;
    $'\x03')
      echo "ctrl_c"
      ;;
    $'\x18')
      echo "ctrl_x"
      ;;
    $'\x7f'|$'\b')
      echo "backspace"
      ;;
    [Nn])
      echo "new_domain"
      ;;
    [Uu])
      echo "update_ssl"
      ;;
    *)
      echo "other"
      ;;
  esac
}

ui_try_read_byte() {
  local __var="$1"
  local timeout_ms="${2:-300}"
  local c=""

  if command -v perl >/dev/null 2>&1; then
    c="$(perl -e '
      use strict;
      use warnings;
      use IO::Select;
      my $ms = shift @ARGV;
      $ms = 300 unless defined $ms;
      my $sel = IO::Select->new(\*STDIN);
      if ($sel->can_read($ms / 1000.0)) {
        my $ch = "";
        my $n = sysread(STDIN, $ch, 1);
        if (defined $n && $n == 1) {
          print $ch;
          exit 0;
        }
      }
      exit 1;
    ' "$timeout_ms" 2>/dev/null)" || return 1
    printf -v "$__var" "%s" "$c"
    return 0
  fi

  # Fallback if perl is unavailable.
  if IFS= read -rsn1 -t 1 c 2>/dev/null; then
    printf -v "$__var" "%s" "$c"
    return 0
  fi
  return 1
}

read_lines_to_array() {
  local __var="$1"
  local line
  eval "$__var=()"
  while IFS= read -r line; do
    eval "$__var+=(\"\$line\")"
  done
}

cert_public_fingerprint() {
  local cert_path="$1"
  openssl x509 -in "$cert_path" -pubkey -noout 2>/dev/null \
    | openssl pkey -pubin -outform DER 2>/dev/null \
    | openssl dgst -sha256 2>/dev/null \
    | awk '{print $2}'
}

key_public_fingerprint() {
  local key_path="$1"
  openssl pkey -in "$key_path" -pubout -outform DER 2>/dev/null \
    | openssl dgst -sha256 2>/dev/null \
    | awk '{print $2}'
}

cert_covers_fqdn() {
  local cert_path="$1"
  local fqdn="$2"

  local text
  if ! text="$(openssl x509 -in "$cert_path" -noout -text 2>/dev/null)"; then
    return 1
  fi

  local escaped_fqdn
  escaped_fqdn="${fqdn//./\\.}"

  if echo "$text" | grep -Eiq "DNS:${escaped_fqdn}(,|$)"; then
    return 0
  fi

  if [[ "$fqdn" == *.*.* ]]; then
    local parent="${fqdn#*.}"
    local escaped_parent
    escaped_parent="${parent//./\\.}"
    if echo "$text" | grep -Eiq "DNS:\*\.${escaped_parent}(,|$)"; then
      return 0
    fi
  fi

  if openssl x509 -in "$cert_path" -noout -subject 2>/dev/null | grep -Eiq "CN=${escaped_fqdn}(,|$)"; then
    return 0
  fi

  return 1
}

expand_user_path() {
  local p="$1"
  if [[ "$p" == "~" ]]; then
    echo "$HOME"
    return 0
  fi
  if [[ "$p" == "~/"* ]]; then
    echo "${HOME}${p:1}"
    return 0
  fi
  echo "$p"
}

find_cert_in_dir() {
  local dir="$1"
  local cert_candidates=(
    fullchain.pem cert.pem certificate.pem certificate.crt cert.crt
    server.crt tls.crt wildcard.crt domain.crt
  )
  local c f

  for c in "${cert_candidates[@]}"; do
    f="${dir%/}/${c}"
    if [[ -f "$f" ]]; then
      echo "$f"
      return 0
    fi
  done

  for f in "${dir%/}"/*.crt "${dir%/}"/*.pem "${dir%/}"/*.cer; do
    [[ -f "$f" ]] || continue
    if openssl x509 -in "$f" -noout >/dev/null 2>&1; then
      echo "$f"
      return 0
    fi
  done
  return 1
}

find_key_in_dir() {
  local dir="$1"
  local key_candidates=(
    privkey.pem private.key key.pem server.key tls.key wildcard.key domain.key
  )
  local c f

  for c in "${key_candidates[@]}"; do
    f="${dir%/}/${c}"
    if [[ -f "$f" ]]; then
      echo "$f"
      return 0
    fi
  done

  for f in "${dir%/}"/*.key "${dir%/}"/*.pem; do
    [[ -f "$f" ]] || continue
    if openssl pkey -in "$f" -noout >/dev/null 2>&1; then
      echo "$f"
      return 0
    fi
  done
  return 1
}

guess_key_from_cert_path() {
  local cert_path="$1"
  local dir base noext
  dir="$(dirname "$cert_path")"
  base="$(basename "$cert_path")"
  noext="${base%.*}"
  local candidates=(
    "${dir}/${noext}.key"
    "${dir}/${noext}.pem"
    "${dir}/${noext}-key.pem"
    "${dir}/${noext}_key.pem"
    "${dir}/privkey.pem"
    "${dir}/private.key"
    "${dir}/key.pem"
  )
  local c
  for c in "${candidates[@]}"; do
    if [[ -f "$c" ]] && openssl pkey -in "$c" -noout >/dev/null 2>&1; then
      echo "$c"
      return 0
    fi
  done
  return 1
}

guess_cert_from_key_path() {
  local key_path="$1"
  local dir base noext
  dir="$(dirname "$key_path")"
  base="$(basename "$key_path")"
  noext="${base%.*}"
  noext="${noext%-key}"
  noext="${noext%_key}"
  local candidates=(
    "${dir}/${noext}.crt"
    "${dir}/${noext}.pem"
    "${dir}/fullchain.pem"
    "${dir}/cert.pem"
    "${dir}/certificate.crt"
    "${dir}/certificate.pem"
  )
  local c
  for c in "${candidates[@]}"; do
    if [[ -f "$c" ]] && openssl x509 -in "$c" -noout >/dev/null 2>&1; then
      echo "$c"
      return 0
    fi
  done
  return 1
}

find_matching_key_for_cert() {
  local cert_path="$1"
  shift
  local cert_fp key_fp dir f
  cert_fp="$(cert_public_fingerprint "$cert_path")"
  [[ -n "$cert_fp" ]] || return 1

  for dir in "$@"; do
    [[ -d "$dir" ]] || continue
    for f in "${dir%/}"/*.key "${dir%/}"/*.pem; do
      [[ -f "$f" ]] || continue
      key_fp="$(key_public_fingerprint "$f")"
      [[ -n "$key_fp" ]] || continue
      if [[ "$key_fp" == "$cert_fp" ]]; then
        echo "$f"
        return 0
      fi
    done
  done
  return 1
}

find_matching_cert_for_key() {
  local key_path="$1"
  shift
  local key_fp cert_fp dir f
  key_fp="$(key_public_fingerprint "$key_path")"
  [[ -n "$key_fp" ]] || return 1

  for dir in "$@"; do
    [[ -d "$dir" ]] || continue
    for f in "${dir%/}"/*.crt "${dir%/}"/*.pem "${dir%/}"/*.cer; do
      [[ -f "$f" ]] || continue
      cert_fp="$(cert_public_fingerprint "$f")"
      [[ -n "$cert_fp" ]] || continue
      if [[ "$cert_fp" == "$key_fp" ]]; then
        echo "$f"
        return 0
      fi
    done
  done
  return 1
}

resolve_existing_cert_key_paths() {
  local cert_input="$1"
  local key_input="$2"
  local __cert_var="$3"
  local __key_var="$4"

  cert_input="$(clean_conf_value "$(trim_spaces "$cert_input")")"
  key_input="$(clean_conf_value "$(trim_spaces "$key_input")")"
  cert_input="$(expand_user_path "$cert_input")"
  key_input="$(expand_user_path "$key_input")"

  local cert_file=""
  local key_file=""
  local dirs=()
  local d existing dir_seen

  if [[ -n "$cert_input" ]]; then
    if [[ -f "$cert_input" ]]; then
      cert_file="$cert_input"
      d="$(dirname "$cert_input")"
      if [[ -d "$d" ]]; then
        dir_seen=0
        for existing in "${dirs[@]-}"; do
          if [[ "$existing" == "$d" ]]; then
            dir_seen=1
            break
          fi
        done
        if [[ "$dir_seen" -eq 0 ]]; then
          dirs+=("$d")
        fi
      fi
    elif [[ -d "$cert_input" ]]; then
      dir_seen=0
      for existing in "${dirs[@]-}"; do
        if [[ "$existing" == "$cert_input" ]]; then
          dir_seen=1
          break
        fi
      done
      if [[ "$dir_seen" -eq 0 ]]; then
        dirs+=("$cert_input")
      fi
    fi
  fi

  if [[ -n "$key_input" ]]; then
    if [[ -f "$key_input" ]]; then
      key_file="$key_input"
      d="$(dirname "$key_input")"
      if [[ -d "$d" ]]; then
        dir_seen=0
        for existing in "${dirs[@]-}"; do
          if [[ "$existing" == "$d" ]]; then
            dir_seen=1
            break
          fi
        done
        if [[ "$dir_seen" -eq 0 ]]; then
          dirs+=("$d")
        fi
      fi
    elif [[ -d "$key_input" ]]; then
      dir_seen=0
      for existing in "${dirs[@]-}"; do
        if [[ "$existing" == "$key_input" ]]; then
          dir_seen=1
          break
        fi
      done
      if [[ "$dir_seen" -eq 0 ]]; then
        dirs+=("$key_input")
      fi
    fi
  fi

  if [[ -n "$cert_file" && -z "$key_file" ]]; then
    key_file="$(guess_key_from_cert_path "$cert_file" || true)"
  fi
  if [[ -n "$key_file" && -z "$cert_file" ]]; then
    cert_file="$(guess_cert_from_key_path "$key_file" || true)"
  fi

  if [[ -z "$cert_file" ]]; then
    for d in "${dirs[@]-}"; do
      cert_file="$(find_cert_in_dir "$d" || true)"
      [[ -n "$cert_file" ]] && break
    done
  fi
  if [[ -z "$key_file" ]]; then
    for d in "${dirs[@]-}"; do
      key_file="$(find_key_in_dir "$d" || true)"
      [[ -n "$key_file" ]] && break
    done
  fi

  if [[ -n "$cert_file" && -z "$key_file" ]]; then
    key_file="$(find_matching_key_for_cert "$cert_file" "${dirs[@]-}" || true)"
  fi
  if [[ -n "$key_file" && -z "$cert_file" ]]; then
    cert_file="$(find_matching_cert_for_key "$key_file" "${dirs[@]-}" || true)"
  fi

  [[ -n "$cert_file" && -f "$cert_file" ]] || return 1
  [[ -n "$key_file" && -f "$key_file" ]] || return 1

  local cert_fp key_fp
  cert_fp="$(cert_public_fingerprint "$cert_file")"
  key_fp="$(key_public_fingerprint "$key_file")"
  if [[ -n "$cert_fp" && -n "$key_fp" && "$cert_fp" != "$key_fp" ]]; then
    local matched_key=""
    matched_key="$(find_matching_key_for_cert "$cert_file" "${dirs[@]-}" || true)"
    if [[ -n "$matched_key" ]]; then
      key_file="$matched_key"
    else
      return 1
    fi
  fi

  printf -v "$__cert_var" "%s" "$cert_file"
  printf -v "$__key_var" "%s" "$key_file"
  return 0
}

collect_matching_cert_pairs() {
  local domain="$1"
  local search_dirs_csv="$2"

  local IFS=','
  local dirs=()
  read -r -a dirs <<< "$search_dirs_csv"

  local certs=()
  local keys=()
  local dir cert key

  if [[ "${#dirs[@]}" -gt 0 ]]; then
    for dir in "${dirs[@]}"; do
      [[ -d "$dir" ]] || continue

      while IFS= read -r cert; do
        certs+=("$cert")
      done < <(find "$dir" -type f \( -name "*.crt" -o -name "*.cer" -o -name "*.pem" \) 2>/dev/null)

      while IFS= read -r key; do
        keys+=("$key")
      done < <(find "$dir" -type f \( -name "*.key" -o -name "*.pem" \) 2>/dev/null)
    done
  fi

  [[ "${#certs[@]}" -gt 0 ]] || return 0
  [[ "${#keys[@]}" -gt 0 ]] || return 0

  local escaped_domain
  escaped_domain="${domain//./\\.}"

  local text cert_fp key_fp
  local seen=()

  for cert in "${certs[@]}"; do
    if ! text="$(openssl x509 -in "$cert" -noout -text 2>/dev/null)"; then
      continue
    fi

    if ! echo "$text" | grep -Eiq "DNS:\*\.${escaped_domain}|DNS:${escaped_domain}"; then
      continue
    fi

    cert_fp="$(cert_public_fingerprint "$cert")"
    [[ -n "$cert_fp" ]] || continue

    for key in "${keys[@]}"; do
      key_fp="$(key_public_fingerprint "$key")"
      [[ -n "$key_fp" ]] || continue

      if [[ "$cert_fp" == "$key_fp" ]]; then
        local pair="${cert}\t${key}"
        local duplicated=0
        local s
        if [[ "${#seen[@]}" -gt 0 ]]; then
          for s in "${seen[@]}"; do
            if [[ "$s" == "$pair" ]]; then
              duplicated=1
              break
            fi
          done
        fi
        if [[ "$duplicated" -eq 0 ]]; then
          seen+=("$pair")
          printf "%s\t%s\n" "$cert" "$key"
        fi
      fi
    done
  done
}

find_godaddy_certs() {
  local domain="$1"
  local search_dirs="$2"

  need_cmd openssl

  local found=0
  local i=0
  local cert key
  while IFS=$'\t' read -r cert key; do
    i=$((i + 1))
    found=1
    printf "[%d] CERT=%s\n" "$i" "$cert"
    printf "    KEY=%s\n" "$key"
  done < <(collect_matching_cert_pairs "$domain" "$search_dirs")

  if [[ "$found" -eq 0 ]]; then
    warn "No matching cert/key pair found for *.${domain} or ${domain}."
  fi
}

get_apache_runtime_info() {
  local cmd out root conf
  local candidates=()
  local pline pbin seen c
  local entry cmdline
  local runtime_root runtime_conf

  parse_apache_cmdline_overrides() {
    local cmdline_in="$1"
    local __root_var="$2"
    local __conf_var="$3"
    local t prev=""
    local parsed_root=""
    local parsed_conf=""

    for t in $cmdline_in; do
      if [[ -n "$prev" ]]; then
        if [[ "$prev" == "-d" ]]; then
          parsed_root="$t"
        elif [[ "$prev" == "-f" ]]; then
          parsed_conf="$t"
        fi
        prev=""
        continue
      fi

      case "$t" in
        -d|-f)
          prev="$t"
          ;;
        -d*)
          parsed_root="${t#-d}"
          ;;
        -f*)
          parsed_conf="${t#-f}"
          ;;
      esac
    done

    # shellcheck disable=SC2178
    printf -v "$__root_var" "%s" "$parsed_root"
    # shellcheck disable=SC2178
    printf -v "$__conf_var" "%s" "$parsed_conf"
  }

  if command -v pgrep >/dev/null 2>&1; then
    while IFS= read -r pline; do
      [[ -n "$pline" ]] || continue
      pbin="$(echo "$pline" | awk '{print $2}')"
      [[ -n "$pbin" ]] || continue
      if [[ "$pbin" == *httpd* || "$pbin" == *apache2* ]]; then
        cmdline="$(echo "$pline" | cut -d' ' -f2-)"
        seen=0
        for c in "${candidates[@]-}"; do
          if [[ "${c%%|*}" == "$pbin" ]]; then
            seen=1
            break
          fi
        done
        if [[ "$seen" -eq 0 ]]; then
          candidates+=("${pbin}|${cmdline}")
        fi
      fi
    done < <(pgrep -af 'httpd|apache2' 2>/dev/null || true)
  fi

  candidates+=("apachectl|" "apache2ctl|" "httpd|")

  for entry in "${candidates[@]-}"; do
    cmd="${entry%%|*}"
    cmdline="${entry#*|}"
    if [[ "$cmd" == */* ]]; then
      [[ -x "$cmd" ]] || continue
      out="$("$cmd" -V 2>/dev/null || true)"
    else
      command -v "$cmd" >/dev/null 2>&1 || continue
      out="$("$cmd" -V 2>/dev/null || true)"
    fi

    root="$(echo "$out" | awk -F'"' '/HTTPD_ROOT=/{print $2; exit}')"
    conf="$(echo "$out" | awk -F'"' '/SERVER_CONFIG_FILE=/{print $2; exit}')"

    runtime_root=""
    runtime_conf=""
    parse_apache_cmdline_overrides "$cmdline" runtime_root runtime_conf
    if [[ -n "$runtime_root" ]]; then
      root="$runtime_root"
    fi
    if [[ -n "$runtime_conf" ]]; then
      conf="$runtime_conf"
    fi

    [[ -n "$root" || -n "$conf" ]] || continue

    if [[ -n "$root" && "$root" != /* ]]; then
      root="$(cd "$root" 2>/dev/null && pwd -P || echo "$root")"
    fi

    if [[ -n "$conf" && "$conf" != /* ]]; then
      if [[ -n "$root" ]]; then
        conf="${root}/${conf}"
      fi
    fi
    if [[ -z "$conf" && -n "$root" ]]; then
      conf="${root}/httpd.conf"
    fi
    if [[ -z "$root" && -n "$conf" ]]; then
      root="$(dirname "$conf")"
    fi
    echo "${cmd}|${root}|${conf}"
    return 0
  done
  return 1
}

apache_list_include_conf_dirs() {
  local main_conf="$1"
  local root="$2"
  local raw p dir

  [[ -f "$main_conf" ]] || return 0

  while IFS= read -r raw; do
    [[ -n "$raw" ]] || continue
    raw="${raw%\"}"
    raw="${raw#\"}"
    raw="${raw%\'}"
    raw="${raw#\'}"
    if [[ "$raw" != /* ]]; then
      p="${root}/${raw}"
    else
      p="$raw"
    fi
    if [[ "$p" == *"*.conf" ]]; then
      dir="${p%/*}"
      [[ -n "$dir" ]] || continue
      echo "$dir"
    fi
  done < <(awk '
    BEGIN { IGNORECASE=1 }
    /^[[:space:]]*#/ { next }
    tolower($1) ~ /^include(optional)?$/ {
      for (i=2; i<=NF; i++) print $i
    }
  ' "$main_conf" 2>/dev/null)
}

ensure_apache_include_dir() {
  local target_dir="$1"
  local info cmd root conf
  info="$(get_apache_runtime_info || true)"
  [[ -n "$info" ]] || return 0
  IFS='|' read -r cmd root conf <<< "$info"
  [[ -f "$conf" ]] || return 0

  mkdir -p "$target_dir"

  local escaped
  escaped="${target_dir//\//\\/}"
  if grep -Eiq "^[[:space:]]*Include(Optional)?[[:space:]]+\"?${escaped}/\\*\\.conf\"?" "$conf"; then
    return 0
  fi

  {
    echo
    echo "# ssl-admin managed include for per-vhost files"
    echo "IncludeOptional \"${target_dir}/*.conf\""
  } >> "$conf"
}

detect_apache_conf_dir() {
  local info cmd root conf
  local apache_dirs=()
  local d

  info="$(get_apache_runtime_info || true)"
  if [[ -n "$info" ]]; then
    IFS='|' read -r cmd root conf <<< "$info"
    # Debian/Ubuntu best write target when a2ensite is available.
    if command -v a2ensite >/dev/null 2>&1 && [[ -n "$root" && -d "${root}/sites-available" ]]; then
      echo "${root}/sites-available"
      return 0
    fi
  fi

  read_lines_to_array apache_dirs < <(detect_apache_conf_dirs)
  for d in "${apache_dirs[@]-}"; do
    case "$d" in
      */sites-available)
        echo "$d"
        return 0
        ;;
    esac
  done
  for d in "${apache_dirs[@]-}"; do
    case "$d" in
      */vhosts|*/servers|*/conf.d|*/sites-enabled)
        echo "$d"
        return 0
        ;;
    esac
  done
  for d in "${apache_dirs[@]-}"; do
    [[ -d "$d" ]] || continue
    echo "$d"
    return 0
  done

  if [[ -d "/etc/apache2/sites-available" ]]; then
    echo "/etc/apache2/sites-available"
  elif [[ -d "/etc/httpd/conf.d" ]]; then
    echo "/etc/httpd/conf.d"
  else
    echo "${DEFAULT_APACHE_PATH}/sites-available"
  fi
}

detect_apache_conf_dirs() {
  local info cmd root conf
  local seen=()
  local candidate_list=()
  local d s dup

  info="$(get_apache_runtime_info || true)"
  if [[ -n "$info" ]]; then
    IFS='|' read -r cmd root conf <<< "$info"
    while IFS= read -r d; do
      [[ -n "$d" ]] || continue
      candidate_list+=("$d")
    done < <(apache_list_include_conf_dirs "$conf" "$root")

    if [[ -n "$root" ]]; then
      candidate_list+=(
        "${root}/sites-enabled"
        "${root}/sites-available"
        "${root}/vhosts"
        "${root}/servers"
        "${root}/conf.d"
        "${root}/extra"
      )
    fi
  fi

  candidate_list+=(
    "/etc/apache2/sites-enabled"
    "/etc/apache2/sites-available"
    "/etc/apache2/conf-enabled"
    "/etc/httpd/conf.d"
    "/etc/httpd/sites-enabled"
    "/etc/httpd/sites-available"
    "/usr/local/etc/httpd/vhosts"
    "/opt/homebrew/etc/httpd/vhosts"
    "${DEFAULT_APACHE_PATH}/sites-enabled"
    "${DEFAULT_APACHE_PATH}/sites-available"
  )

  for d in "${candidate_list[@]-}"; do
    [[ -n "$d" && -d "$d" ]] || continue
    dup=0
    for s in "${seen[@]-}"; do
      if [[ "$s" == "$d" ]]; then
        dup=1
        break
      fi
    done
    if [[ "$dup" -eq 0 ]]; then
      seen+=("$d")
    fi
  done

  for d in "${seen[@]-}"; do
    echo "$d"
  done
}

detect_apache_active_conf_dirs() {
  local info cmd root conf
  local candidate_list=()
  local seen=()
  local d s dup

  info="$(get_apache_runtime_info || true)"
  if [[ -n "$info" ]]; then
    IFS='|' read -r cmd root conf <<< "$info"
    while IFS= read -r d; do
      [[ -n "$d" ]] || continue
      candidate_list+=("$d")
    done < <(apache_list_include_conf_dirs "$conf" "$root")

    if [[ -n "$root" ]]; then
      candidate_list+=(
        "${root}/sites-enabled"
        "${root}/conf-enabled"
        "${root}/conf.d"
        "${root}/servers"
        "${root}/vhosts"
      )
    fi
  fi

  candidate_list+=(
    "/etc/apache2/sites-enabled"
    "/etc/apache2/conf-enabled"
    "/etc/httpd/conf.d"
    "/etc/httpd/sites-enabled"
  )

  for d in "${candidate_list[@]-}"; do
    [[ -n "$d" && -d "$d" ]] || continue
    dup=0
    for s in "${seen[@]-}"; do
      if [[ "$s" == "$d" ]]; then
        dup=1
        break
      fi
    done
    if [[ "$dup" -eq 0 ]]; then
      seen+=("$d")
    fi
  done

  for d in "${seen[@]-}"; do
    echo "$d"
  done
}

enable_apache_modules_if_possible() {
  local mode="$1"
  if ! command -v a2enmod >/dev/null 2>&1; then
    return 0
  fi

  local mods=(ssl rewrite headers)
  if [[ "$mode" == "proxy" ]]; then
    mods+=(proxy proxy_http)
  fi

  local mod
  for mod in "${mods[@]}"; do
    a2enmod "$mod" >/dev/null 2>&1 || true
  done
}

enable_apache_site_if_needed() {
  local conf_file="$1"
  if command -v a2ensite >/dev/null 2>&1; then
    a2ensite "$(basename "$conf_file")" >/dev/null 2>&1 || true
  fi
}

disable_apache_site_if_needed() {
  local conf_file="$1"
  if command -v a2dissite >/dev/null 2>&1; then
    a2dissite "$(basename "$conf_file")" >/dev/null 2>&1 || true
  fi
}

test_and_reload_nginx() {
  need_cmd nginx
  nginx -t

  if command -v systemctl >/dev/null 2>&1; then
    if systemctl reload nginx >/dev/null 2>&1; then
      return 0
    fi
  fi
  if command -v service >/dev/null 2>&1; then
    if service nginx reload >/dev/null 2>&1; then
      return 0
    fi
  fi

  nginx -s reload >/dev/null 2>&1
}

test_and_reload_apache() {
  if command -v apache2ctl >/dev/null 2>&1; then
    apache2ctl -t
  elif command -v apachectl >/dev/null 2>&1; then
    apachectl -t
  elif command -v httpd >/dev/null 2>&1; then
    httpd -t
  else
    die "Apache binary not found (apache2ctl/apachectl/httpd)."
  fi

  if command -v systemctl >/dev/null 2>&1; then
    if systemctl reload apache2 >/dev/null 2>&1; then
      return 0
    fi
    if systemctl reload httpd >/dev/null 2>&1; then
      return 0
    fi
  fi
  if command -v service >/dev/null 2>&1; then
    if service apache2 reload >/dev/null 2>&1; then
      return 0
    fi
    if service httpd reload >/dev/null 2>&1; then
      return 0
    fi
  fi

  if command -v apachectl >/dev/null 2>&1; then
    apachectl graceful >/dev/null 2>&1
    return 0
  fi
  if command -v apache2ctl >/dev/null 2>&1; then
    apache2ctl graceful >/dev/null 2>&1
    return 0
  fi

  die "Could not reload Apache service."
}

try_reload_apache_nonfatal() {
  local ok_reload=0

  if command -v apache2ctl >/dev/null 2>&1; then
    if ! apache2ctl -t >/dev/null 2>&1; then
      warn "Apache config has errors after delete (possibly unrelated vhost). Manual fix required before reload."
      return 1
    fi
  elif command -v apachectl >/dev/null 2>&1; then
    if ! apachectl -t >/dev/null 2>&1; then
      warn "Apache config has errors after delete (possibly unrelated vhost). Manual fix required before reload."
      return 1
    fi
  elif command -v httpd >/dev/null 2>&1; then
    if ! httpd -t >/dev/null 2>&1; then
      warn "Apache config has errors after delete (possibly unrelated vhost). Manual fix required before reload."
      return 1
    fi
  else
    warn "Apache binary not found for reload."
    return 1
  fi

  if command -v systemctl >/dev/null 2>&1; then
    if systemctl reload apache2 >/dev/null 2>&1 || systemctl reload httpd >/dev/null 2>&1; then
      ok_reload=1
    fi
  fi
  if [[ "$ok_reload" -eq 0 ]] && command -v service >/dev/null 2>&1; then
    if service apache2 reload >/dev/null 2>&1 || service httpd reload >/dev/null 2>&1; then
      ok_reload=1
    fi
  fi
  if [[ "$ok_reload" -eq 0 ]] && command -v apachectl >/dev/null 2>&1; then
    if apachectl graceful >/dev/null 2>&1; then
      ok_reload=1
    fi
  fi
  if [[ "$ok_reload" -eq 0 ]] && command -v apache2ctl >/dev/null 2>&1; then
    if apache2ctl graceful >/dev/null 2>&1; then
      ok_reload=1
    fi
  fi

  if [[ "$ok_reload" -eq 1 ]]; then
    log "Apache reloaded."
    return 0
  fi
  warn "Could not reload Apache automatically after delete."
  return 1
}

try_reload_nginx_nonfatal() {
  local ok_reload=0
  if command -v nginx >/dev/null 2>&1; then
    if ! nginx -t >/dev/null 2>&1; then
      warn "Nginx config has errors after delete (possibly unrelated vhost). Manual fix required before reload."
      return 1
    fi
  else
    warn "nginx binary not found for reload."
    return 1
  fi

  if command -v systemctl >/dev/null 2>&1; then
    if systemctl reload nginx >/dev/null 2>&1; then
      ok_reload=1
    fi
  fi
  if [[ "$ok_reload" -eq 0 ]] && command -v service >/dev/null 2>&1; then
    if service nginx reload >/dev/null 2>&1; then
      ok_reload=1
    fi
  fi
  if [[ "$ok_reload" -eq 0 ]]; then
    if nginx -s reload >/dev/null 2>&1; then
      ok_reload=1
    fi
  fi

  if [[ "$ok_reload" -eq 1 ]]; then
    log "Nginx reloaded."
    return 0
  fi
  warn "Could not reload Nginx automatically after delete."
  return 1
}

disable_apache_vhost_file_nonfatal() {
  local file="$1"
  local base ts target

  if command -v a2dissite >/dev/null 2>&1; then
    if [[ "$file" == /etc/apache2/sites-enabled/* || "$file" == /etc/apache2/sites-available/* ]]; then
      base="$(basename "$file")"
      a2dissite "$base" >/dev/null 2>&1 || true
    fi
  fi

  if [[ -L "$file" ]]; then
    rm -f "$file"
    log "Disabled Apache vhost symlink: $file"
    return 0
  fi

  if [[ -f "$file" ]]; then
    ts="$(date +%Y%m%d%H%M%S)"
    target="${file}.disabled.${ts}"
    mv "$file" "$target"
    log "Disabled Apache vhost file: ${file} -> ${target}"
  fi
}

doctor_apache_ssl() {
  local apply=0
  local reload=0

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --apply) apply=1; shift ;;
      --reload) reload=1; shift ;;
      --help|-h)
        echo "doctor options:"
        echo "  --apply   disable Apache vhosts with broken SSL file paths"
        echo "  --reload  reload Apache after --apply"
        return 0
        ;;
      *)
        die "Unknown doctor option: $1"
        ;;
    esac
  done

  if [[ "$apply" -eq 1 || "$reload" -eq 1 ]]; then
    require_root
  fi

  local dirs=()
  read_lines_to_array dirs < <(detect_apache_active_conf_dirs)
  if [[ "${#dirs[@]}" -eq 0 ]]; then
    warn "No active Apache config directories found."
    return 0
  fi

  local seen_files=()
  local broken_rows=()
  local dir file real_file s dup
  local server_name cert key issue
  local count=0

  for dir in "${dirs[@]-}"; do
    [[ -d "$dir" ]] || continue
    for file in "$dir"/*; do
      is_vhost_candidate_file "$file" || continue

      # Keep vhost-like files only.
      if ! awk '
        BEGIN { IGNORECASE=1; ok=0 }
        /^[[:space:]]*#/ { next }
        /<VirtualHost[[:space:]>]/ { ok=1 }
        tolower($1)=="servername" { ok=1 }
        END { exit ok ? 0 : 1 }
      ' "$file" 2>/dev/null; then
        continue
      fi

      real_file="$(cd "$(dirname "$file")" 2>/dev/null && pwd -P)/$(basename "$file")"
      dup=0
      for s in "${seen_files[@]-}"; do
        if [[ "$s" == "$real_file" ]]; then
          dup=1
          break
        fi
      done
      if [[ "$dup" -eq 1 ]]; then
        continue
      fi
      seen_files+=("$real_file")

      server_name="$(awk 'tolower($1)=="servername"{print $2; exit}' "$file" 2>/dev/null || true)"
      server_name="$(clean_conf_value "$server_name")"
      [[ -n "$server_name" ]] || server_name="-"

      cert="$(awk 'tolower($1)=="sslcertificatefile"{print $2; exit}' "$file" 2>/dev/null || true)"
      key="$(awk 'tolower($1)=="sslcertificatekeyfile"{print $2; exit}' "$file" 2>/dev/null || true)"
      cert="$(clean_conf_value "$cert")"
      key="$(clean_conf_value "$key")"

      issue=""
      if [[ -n "$cert" && ! -f "$cert" ]]; then
        issue="CERT_MISSING"
      fi
      if [[ -n "$key" && ! -f "$key" ]]; then
        if [[ -n "$issue" ]]; then
          issue="${issue}+KEY_MISSING"
        else
          issue="KEY_MISSING"
        fi
      fi
      [[ -n "$issue" ]] || continue

      broken_rows+=("${server_name}"$'\x1f'"${file}"$'\x1f'"${cert:--}"$'\x1f'"${key:--}"$'\x1f'"${issue}")
      count=$((count + 1))
    done
  done

  echo "Apache SSL Doctor"
  echo "Active config dirs:"
  local d
  for d in "${dirs[@]-}"; do
    echo "  - $d"
  done
  echo

  if [[ "$count" -eq 0 ]]; then
    log "No Apache SSL path errors found."
    return 0
  fi

  printf "%-30s %-55s %-20s\n" "DOMAIN" "VHOST_FILE" "ISSUE"
  printf "%-30s %-55s %-20s\n" "------------------------------" "-------------------------------------------------------" "--------------------"
  local row r_domain r_file r_cert r_key r_issue
  for row in "${broken_rows[@]-}"; do
    [[ -n "$row" ]] || continue
    IFS=$'\x1f' read -r r_domain r_file r_cert r_key r_issue <<< "$row"
    printf "%-30s %-55s %-20s\n" "$r_domain" "$r_file" "$r_issue"
    echo "  cert: $r_cert"
    echo "  key : $r_key"
  done
  echo

  if [[ "$apply" -eq 0 ]]; then
    warn "Run with --apply to disable broken Apache vhosts automatically."
    return 1
  fi

  log "Applying fixes (disable broken Apache vhosts)..."
  for row in "${broken_rows[@]-}"; do
    [[ -n "$row" ]] || continue
    IFS=$'\x1f' read -r r_domain r_file r_cert r_key r_issue <<< "$row"
    disable_apache_vhost_file_nonfatal "$r_file"
  done

  if [[ "$reload" -eq 1 ]]; then
    try_reload_apache_nonfatal || true
  fi

  log "Apache SSL doctor apply completed."
}

server_reload() {
  local server="$1"
  if [[ "$server" == "nginx" ]]; then
    test_and_reload_nginx
  else
    test_and_reload_apache
  fi
}

get_vhost_file() {
  local server="$1"
  local fqdn="$2"
  if [[ "$server" == "nginx" ]]; then
    echo "${DEFAULT_NGINX_CONF_DIR}/${fqdn}.conf"
  else
    local apache_dir
    apache_dir="$(detect_apache_conf_dir)"
    echo "${apache_dir}/${fqdn}.conf"
  fi
}

find_apache_vhost_files_by_fqdn() {
  local fqdn="$1"
  local dirs=()
  local dir file server_name matched
  local seen=()
  local s dup

  read_lines_to_array dirs < <(detect_apache_conf_dirs)
  dirs+=(
    "/etc/apache2/sites-enabled"
    "/etc/apache2/sites-available"
    "/etc/httpd/conf.d"
    "/etc/httpd/sites-enabled"
    "/etc/httpd/sites-available"
  )

  for dir in "${dirs[@]-}"; do
    [[ -d "$dir" ]] || continue
    for file in "$dir"/*; do
      is_vhost_candidate_file "$file" || continue
      server_name="$(awk 'tolower($1)=="servername"{print $2; exit}' "$file" 2>/dev/null || true)"
      server_name="$(clean_conf_value "$server_name")"
      matched=0
      if [[ -n "$server_name" && "$server_name" == "$fqdn" ]]; then
        matched=1
      else
        if awk -v fq="$fqdn" '
          BEGIN { IGNORECASE=1; ok=0 }
          /^[[:space:]]*#/ { next }
          tolower($1)=="serveralias" {
            for (i=2; i<=NF; i++) {
              gsub(/;$/,"",$i)
              if ($i == fq) { ok=1; exit }
            }
          }
          END { exit ok ? 0 : 1 }
        ' "$file" 2>/dev/null; then
          matched=1
        fi
      fi
      [[ "$matched" -eq 1 ]] || continue

      dup=0
      for s in "${seen[@]-}"; do
        if [[ "$s" == "$file" ]]; then
          dup=1
          break
        fi
      done
      if [[ "$dup" -eq 0 ]]; then
        seen+=("$file")
      fi
    done
  done

  for s in "${seen[@]-}"; do
    printf "%s\n" "$s"
  done
}

find_nginx_vhost_files_by_fqdn() {
  local fqdn="$1"
  local dir="$DEFAULT_NGINX_CONF_DIR"
  local file line tok matched

  [[ -d "$dir" ]] || return 0
  for file in "$dir"/*; do
    is_vhost_candidate_file "$file" || continue
    matched=0
    while IFS= read -r line; do
      case "$line" in
        *server_name*)
          for tok in $line; do
            tok="${tok%;}"
            [[ "$tok" == "server_name" ]] && continue
            [[ -n "$tok" ]] || continue
            if [[ "$tok" == "$fqdn" ]]; then
              matched=1
              break
            fi
          done
          ;;
      esac
      [[ "$matched" -eq 1 ]] && break
    done < "$file"
    if [[ "$matched" -eq 1 ]]; then
      printf "%s\n" "$file"
    fi
  done
}

backup_if_exists() {
  local file="$1"
  if [[ -f "$file" ]]; then
    local ts
    ts="$(date +%Y%m%d%H%M%S)"
    cp "$file" "${file}.bak.${ts}"
  fi
}

ensure_can_overwrite_vhost() {
  local file="$1"
  local force="$2"
  local interactive="$3"

  if [[ -f "$file" && "$force" -ne 1 ]]; then
    if [[ "$interactive" -eq 1 ]]; then
      if ask_yes_no "Vhost exists: ${file}. Overwrite?" "no"; then
        return 0
      fi
    fi
    die "Vhost already exists: ${file}. Use --force to overwrite."
  fi
}

write_nginx_challenge_vhost() {
  local file="$1"
  local fqdn="$2"
  local acme_webroot="$3"

  mkdir -p "$(dirname "$file")"
  mkdir -p "${acme_webroot}/.well-known/acme-challenge"

  cat > "$file" <<EOF_NGX
server {
    listen 80;
    listen [::]:80;
    server_name ${fqdn};

    location /.well-known/acme-challenge/ {
        root ${acme_webroot};
        try_files \$uri =404;
    }

    location / {
        return 200 "acme challenge endpoint\n";
        add_header Content-Type text/plain;
    }
}
EOF_NGX
}

write_nginx_final_vhost() {
  local file="$1"
  local fqdn="$2"
  local mode="$3"
  local upstream="$4"
  local docroot="$5"
  local cert_file="$6"
  local key_file="$7"
  local acme_webroot="$8"

  local upstream_clean
  upstream_clean="$(trim_trailing_slash "$upstream")"

  mkdir -p "$(dirname "$file")"
  if [[ -n "$acme_webroot" ]]; then
    mkdir -p "${acme_webroot}/.well-known/acme-challenge"
  fi

  if [[ "$mode" == "static" ]]; then
    cat > "$file" <<EOF_NGX
server {
    listen 80;
    listen [::]:80;
    server_name ${fqdn};

    location /.well-known/acme-challenge/ {
        root ${acme_webroot};
        try_files \$uri =404;
    }

    location / {
        return 301 https://\$host\$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${fqdn};

    ssl_certificate ${cert_file};
    ssl_certificate_key ${key_file};
    ssl_protocols TLSv1.2 TLSv1.3;

    root ${docroot};
    index index.html index.htm;

    location / {
        try_files \$uri \$uri/ =404;
    }
}
EOF_NGX
  else
    cat > "$file" <<EOF_NGX
server {
    listen 80;
    listen [::]:80;
    server_name ${fqdn};

    location /.well-known/acme-challenge/ {
        root ${acme_webroot};
        try_files \$uri =404;
    }

    location / {
        return 301 https://\$host\$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${fqdn};

    ssl_certificate ${cert_file};
    ssl_certificate_key ${key_file};
    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass ${upstream_clean};
        proxy_http_version 1.1;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }
}
EOF_NGX
  fi
}

write_apache_challenge_vhost() {
  local file="$1"
  local fqdn="$2"
  local acme_webroot="$3"

  mkdir -p "$(dirname "$file")"
  mkdir -p "${acme_webroot}/.well-known/acme-challenge"

  cat > "$file" <<EOF_AP
<VirtualHost *:80>
    ServerName ${fqdn}

    Alias /.well-known/acme-challenge/ "${acme_webroot}/.well-known/acme-challenge/"
    <Directory "${acme_webroot}/.well-known/acme-challenge/">
        Options None
        AllowOverride None
        Require all granted
    </Directory>

    DocumentRoot /var/www/html
</VirtualHost>
EOF_AP
}

write_apache_final_vhost() {
  local file="$1"
  local fqdn="$2"
  local mode="$3"
  local upstream="$4"
  local docroot="$5"
  local cert_file="$6"
  local key_file="$7"
  local acme_webroot="$8"

  local upstream_clean
  upstream_clean="$(trim_trailing_slash "$upstream")"

  mkdir -p "$(dirname "$file")"
  mkdir -p "${acme_webroot}/.well-known/acme-challenge"

  if [[ "$mode" == "static" ]]; then
    cat > "$file" <<EOF_AP
<VirtualHost *:80>
    ServerName ${fqdn}

    Alias /.well-known/acme-challenge/ "${acme_webroot}/.well-known/acme-challenge/"
    <Directory "${acme_webroot}/.well-known/acme-challenge/">
        Options None
        AllowOverride None
        Require all granted
    </Directory>

    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerName ${fqdn}

    SSLEngine on
    SSLCertificateFile ${cert_file}
    SSLCertificateKeyFile ${key_file}
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1

    DocumentRoot ${docroot}
    <Directory ${docroot}>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-Content-Type-Options "nosniff"
</VirtualHost>
</IfModule>
EOF_AP
  else
    cat > "$file" <<EOF_AP
<VirtualHost *:80>
    ServerName ${fqdn}

    Alias /.well-known/acme-challenge/ "${acme_webroot}/.well-known/acme-challenge/"
    <Directory "${acme_webroot}/.well-known/acme-challenge/">
        Options None
        AllowOverride None
        Require all granted
    </Directory>

    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerName ${fqdn}

    SSLEngine on
    SSLCertificateFile ${cert_file}
    SSLCertificateKeyFile ${key_file}
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1

    ProxyPreserveHost On
    ProxyRequests Off
    RequestHeader set X-Forwarded-Proto "https"
    ProxyPass / ${upstream_clean}/
    ProxyPassReverse / ${upstream_clean}/

    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-Content-Type-Options "nosniff"
</VirtualHost>
</IfModule>
EOF_AP
  fi
}

validate_dns_for_letsencrypt() {
  local fqdn="$1"

  local resolved_ip
  resolved_ip="$(resolve_fqdn_ipv4 "$fqdn")"

  if [[ -z "$resolved_ip" ]]; then
    die "Domain ${fqdn} does not resolve in DNS. LetsEncrypt requires public DNS."
  fi

  if is_private_or_loopback_ipv4 "$resolved_ip"; then
    die "Domain ${fqdn} resolves to private/loopback IP (${resolved_ip}). LetsEncrypt HTTP-01 will fail."
  fi

  log "DNS for ${fqdn} resolves to ${resolved_ip}"
}

issue_letsencrypt_cert() {
  local fqdn="$1"
  local email="$2"
  local acme_webroot="$3"

  need_cmd certbot

  if [[ -z "$email" ]]; then
    die "LetsEncrypt requires --email (or interactive email input)."
  fi

  mkdir -p "${acme_webroot}/.well-known/acme-challenge"

  certbot certonly \
    --webroot \
    -w "$acme_webroot" \
    -d "$fqdn" \
    --non-interactive \
    --agree-tos \
    --email "$email" \
    --keep-until-expiring
}

fqdn_looks_local_dev() {
  local fqdn="$1"
  case "$fqdn" in
    localhost|*.localhost|*.local|*.test|*.internal|*.home.arpa|*.lan)
      return 0
      ;;
  esac
  return 1
}

issue_mkcert_cert() {
  local fqdn="$1"
  local cert_file="$2"
  local key_file="$3"

  need_cmd mkcert

  mkdir -p "$(dirname "$cert_file")" "$(dirname "$key_file")"

  # Try to install local CA if needed; continue if already installed or unsupported.
  mkcert -install >/dev/null 2>&1 || true

  mkcert -cert-file "$cert_file" -key-file "$key_file" "$fqdn"

  [[ -f "$cert_file" ]] || die "mkcert certificate not found at ${cert_file}"
  [[ -f "$key_file" ]] || die "mkcert key not found at ${key_file}"
}

cert_issuer_is_mkcert() {
  local cert_file="$1"
  [[ -f "$cert_file" ]] || return 1
  if ! command -v openssl >/dev/null 2>&1; then
    return 1
  fi
  openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | grep -Eiq 'mkcert'
}

print_usage() {
  cat <<'USAGE'
ssl-admin v2

TUI keys:
  ↑/↓ move
  ENTER select
  BACKSPACE delete selected/current vhost from main list
  SPACE multi-select (when available)
  N create new domain/subdomain
  Ctrl+C cancel current operation
  Ctrl+X exit

Commands:
  create              Create/update domain or subdomain vhost with SSL
  dashboard           One-screen SSL summary + renewal recommendation
  status              Show SSL status and expiration per vhost
  renew               Renew certificates close to expiration (LetsEncrypt and optional mkcert)
  list                List current vhosts and SSL cert inventories
  doctor              Diagnose broken Apache SSL cert/key paths (optional auto-fix)
  find-godaddy-certs  Find local cert/key pairs for GoDaddy wildcard certs
  migrate             Build JSON inventory from existing vhosts and certs
  delete              Delete vhost (and optional LetsEncrypt/mkcert cert assets)
  menu                Interactive menu (default)
  help                Show this help

create options:
  --fqdn FQDN
  --domain DOMAIN
  --subdomain LABEL                 (use @ for root domain)
  --server apache|nginx             (default: apache)
  --mode static|proxy               (default: static)
  --upstream URL                    (required for proxy)
  --document-root PATH              (default: /var/www/<fqdn>/public)
  --cert-provider auto|custom|godaddy|letsencrypt|mkcert  (default: auto)
  --cert PATH                       (manual cert path)
  --key PATH                        (manual key path)
  --godaddy-domain DOMAIN           (default: pixellar.pe)
  --search-dirs CSV                 (default: /etc/ssl,/etc/pki,/etc/apache2,/etc/httpd)
  --cert-index N                    (default: 1)
  --email EMAIL                     (for letsencrypt)
  --acme-webroot PATH               (default: /var/www/_letsencrypt)
  --force
  --non-interactive

status options:
  --warn-days N                    (default: 30)
  --fqdn FQDN                      (optional filter)

dashboard options:
  --warn-days N                    (default: 30)
  --godaddy-domain DOMAIN          (default: pixellar.pe)
  --search-dirs CSV                (default: /etc/ssl,/etc/pki,/etc/apache2,/etc/httpd)
  --json                           (machine-readable output)
  --output PATH                    (only with --json; writes JSON file)

renew options:
  --warn-days N                    (default: 30)
  --fqdn FQDN                      (optional filter, repeatable)
  --dry-run
  --force-renewal                  (force renew each due cert)
  --include-mkcert                 (include mkcert certificates in renewal flow)
  --mkcert-only                    (renew only mkcert certificates)
  --godaddy-domain DOMAIN          (default: pixellar.pe; for provider discovery)
  --search-dirs CSV                (default: /etc/ssl,/etc/pki,/etc/apache2,/etc/httpd)
  --no-reload                      (skip Apache/Nginx reload after renew)

find-godaddy-certs options:
  --domain DOMAIN
  --search-dirs CSV

migrate options:
  --output PATH                     (default: /var/tmp/ssl-admin-migrate-<timestamp>.json)
  --godaddy-domain DOMAIN           (default: pixellar.pe)
  --search-dirs CSV                 (default: /etc/ssl,/etc/pki,/etc/apache2,/etc/httpd)

delete options:
  --fqdn FQDN
  --server apache|nginx|auto        (default: auto)
  --vhost-file PATH                 (optional explicit vhost file to remove)
  --delete-letsencrypt
  --delete-mkcert
  --force
  --non-interactive

doctor options:
  --apply                           disable broken Apache SSL vhosts automatically
  --reload                          reload Apache after --apply

Examples:
  sudo ./ssl-admin.sh create --fqdn api.pixellar.pe --server apache --mode proxy --upstream http://127.0.0.1:3000
  sudo ./ssl-admin.sh dashboard --warn-days 30
  sudo ./ssl-admin.sh dashboard --warn-days 30 --json --output /var/tmp/ssl-dashboard.json
  sudo ./ssl-admin.sh status --warn-days 21
  sudo ./ssl-admin.sh renew --warn-days 15
  sudo ./ssl-admin.sh renew --warn-days 15 --include-mkcert
  sudo ./ssl-admin.sh create --fqdn app.pixellar.pe --server nginx --mode static --document-root /var/www/app/public --cert-provider godaddy
  sudo ./ssl-admin.sh create --fqdn demo.example.com --server apache --cert-provider letsencrypt --email ops@example.com
  sudo ./ssl-admin.sh create --fqdn app.local --server apache --cert-provider mkcert
  sudo ./ssl-admin.sh find-godaddy-certs --domain pixellar.pe
  sudo ./ssl-admin.sh migrate --output /var/tmp/ssl-inventory.json
  sudo ./ssl-admin.sh delete --fqdn api.pixellar.pe --server apache --delete-letsencrypt
  sudo ./ssl-admin.sh delete --fqdn app.local --server apache --delete-mkcert
USAGE
}

build_fqdn() {
  local fqdn="$1"
  local domain="$2"
  local subdomain="$3"

  if [[ -n "$fqdn" ]]; then
    fqdn="$(normalize_lower "$fqdn")"
    fqdn="${fqdn%.}"
    echo "$fqdn"
    return 0
  fi

  domain="$(normalize_lower "$domain")"
  subdomain="$(normalize_lower "$subdomain")"

  [[ -n "$domain" ]] || die "Missing domain or fqdn."

  if [[ -z "$subdomain" || "$subdomain" == "@" ]]; then
    echo "$domain"
  else
    echo "${subdomain}.${domain}"
  fi
}

pick_godaddy_pair() {
  local godaddy_domain="$1"
  local search_dirs="$2"
  local cert_index="$3"
  local interactive="$4"
  local __cert_var="$5"
  local __key_var="$6"

  local pairs=()
  read_lines_to_array pairs < <(collect_matching_cert_pairs "$godaddy_domain" "$search_dirs")

  if [[ "${#pairs[@]}" -eq 0 ]]; then
    return 1
  fi

  local selected_index="$cert_index"

  if [[ "$selected_index" -lt 1 || "$selected_index" -gt "${#pairs[@]}" ]]; then
    if [[ "$interactive" -eq 1 ]]; then
      warn "cert-index out of range. Available pairs: ${#pairs[@]}"
      find_godaddy_certs "$godaddy_domain" "$search_dirs"
      while true; do
        ask_text selected_index "Select cert index" "1"
        if [[ "$selected_index" =~ ^[0-9]+$ ]] && (( selected_index >= 1 && selected_index <= ${#pairs[@]} )); then
          break
        fi
        warn "Invalid index."
      done
    else
      die "--cert-index ${selected_index} out of range. Found ${#pairs[@]} pair(s)."
    fi
  fi

  local cert key
  IFS=$'\t' read -r cert key <<< "${pairs[$((selected_index - 1))]}"
  printf -v "$__cert_var" "%s" "$cert"
  printf -v "$__key_var" "%s" "$key"
  return 0
}

create_site() {
  # run_menu operates with `set +e`; enforce strict failure here to avoid
  # continuing after cert/key validation errors and generating broken vhosts.
  local __had_errexit=0
  if [[ "$-" == *e* ]]; then
    __had_errexit=1
  fi
  set -e
  trap 'if [[ "$__had_errexit" -eq 0 ]]; then set +e; fi; trap - RETURN' RETURN

  local fqdn=""
  local domain=""
  local subdomain=""
  local server="$DEFAULT_SERVER"
  local mode="$DEFAULT_MODE"
  local upstream=""
  local document_root=""
  local cert_provider="auto"
  local cert_file=""
  local key_file=""
  local godaddy_domain="$DEFAULT_GODADDY_DOMAIN"
  local search_dirs="$DEFAULT_GODADDY_SEARCH_DIRS"
  local cert_index=1
  local email=""
  local acme_webroot="$DEFAULT_ACME_WEBROOT"
  local force=0
  local non_interactive=0

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --fqdn) fqdn="${2:-}"; shift 2 ;;
      --domain) domain="${2:-}"; shift 2 ;;
      --subdomain) subdomain="${2:-}"; shift 2 ;;
      --server) server="${2:-}"; shift 2 ;;
      --mode) mode="${2:-}"; shift 2 ;;
      --upstream) upstream="${2:-}"; shift 2 ;;
      --document-root) document_root="${2:-}"; shift 2 ;;
      --cert-provider) cert_provider="${2:-}"; shift 2 ;;
      --cert) cert_file="${2:-}"; shift 2 ;;
      --key) key_file="${2:-}"; shift 2 ;;
      --godaddy-domain) godaddy_domain="${2:-}"; shift 2 ;;
      --search-dirs) search_dirs="${2:-}"; shift 2 ;;
      --cert-index) cert_index="${2:-}"; shift 2 ;;
      --email) email="${2:-}"; shift 2 ;;
      --acme-webroot) acme_webroot="${2:-}"; shift 2 ;;
      --force) force=1; shift ;;
      --non-interactive) non_interactive=1; shift ;;
      --help|-h)
        print_usage
        exit 0
        ;;
      *)
        die "Unknown create option: $1"
        ;;
    esac
  done

  require_root
  need_cmd openssl

  server="$(normalize_lower "$server")"
  mode="$(normalize_lower "$mode")"
  cert_provider="$(normalize_lower "$cert_provider")"
  godaddy_domain="$(normalize_lower "$godaddy_domain")"

  [[ "$server" == "apache" || "$server" == "nginx" ]] || die "--server must be apache or nginx"
  [[ "$mode" == "static" || "$mode" == "proxy" ]] || die "--mode must be static or proxy"
  [[ "$cert_provider" == "auto" || "$cert_provider" == "custom" || "$cert_provider" == "godaddy" || "$cert_provider" == "letsencrypt" || "$cert_provider" == "mkcert" ]] || die "--cert-provider must be auto|custom|godaddy|letsencrypt|mkcert"
  [[ "$cert_index" =~ ^[0-9]+$ ]] || die "--cert-index must be a positive integer"
  (( cert_index >= 1 )) || die "--cert-index must be >= 1"

  if [[ "$non_interactive" -eq 0 ]]; then
    if [[ -z "$fqdn" && ( -z "$domain" || -z "$subdomain" ) ]]; then
      ask_text domain "Base domain" "${domain:-$godaddy_domain}"
      ask_text subdomain "Subdomain label (use @ for root domain)" "${subdomain:-api}"
    fi
    ask_choice server "Web server" "$server" apache nginx
    ask_choice mode "Site mode" "$mode" static proxy
    if [[ "$mode" == "proxy" && -z "$upstream" ]]; then
      ask_text upstream "Upstream URL" "http://127.0.0.1:3000"
    fi
    if [[ "$mode" == "static" && -z "$document_root" ]]; then
      local suggested_fqdn
      suggested_fqdn="$(build_fqdn "$fqdn" "$domain" "$subdomain")"
      ask_text document_root "DocumentRoot" "${DEFAULT_WEBROOT_BASE}/${suggested_fqdn}/public"
    fi
    if [[ -z "$cert_file" && -z "$key_file" ]]; then
      ask_choice cert_provider "Certificate provider" "$cert_provider" auto custom godaddy letsencrypt mkcert
    fi
    if [[ "$cert_provider" == "custom" ]]; then
      local custom_cert_input="$cert_file"
      local custom_key_input="$key_file"
      local resolved_custom_cert=""
      local resolved_custom_key=""
      ask_text custom_cert_input "Ruta cert (archivo o carpeta)" "$custom_cert_input"
      ask_text custom_key_input "Ruta key (archivo o carpeta)" "$custom_key_input"
      if ! resolve_existing_cert_key_paths "$custom_cert_input" "$custom_key_input" resolved_custom_cert resolved_custom_key; then
        die "No se pudo resolver un par cert/key valido para provider=custom."
      fi
      cert_file="$resolved_custom_cert"
      key_file="$resolved_custom_key"
    fi
  fi

  fqdn="$(build_fqdn "$fqdn" "$domain" "$subdomain")"
  [[ -n "$fqdn" ]] || die "Could not resolve FQDN"

  if ! is_valid_fqdn "$fqdn"; then
    die "Invalid FQDN: $fqdn"
  fi

  if [[ "$mode" == "proxy" && -z "$upstream" ]]; then
    die "--upstream is required when mode=proxy"
  fi

  if [[ "$mode" == "static" ]]; then
    if [[ -z "$document_root" ]]; then
      document_root="${DEFAULT_WEBROOT_BASE}/${fqdn}/public"
    fi
    mkdir -p "$document_root"
    if [[ ! -f "$document_root/index.html" ]]; then
      cat > "$document_root/index.html" <<HTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${fqdn}</title>
</head>
<body>
  <h1>${fqdn} ready</h1>
  <p>Provisioned by ssl-admin v2.</p>
</body>
</html>
HTML
    fi
  fi

  local selected_provider="$cert_provider"

  if [[ -n "$cert_file" || -n "$key_file" ]]; then
    [[ -n "$cert_file" && -n "$key_file" ]] || die "Provide both --cert and --key, or neither"
    [[ -f "$cert_file" ]] || die "Certificate file not found: $cert_file"
    [[ -f "$key_file" ]] || die "Private key file not found: $key_file"
    selected_provider="custom"
  fi

  if [[ "$selected_provider" == "custom" ]]; then
    [[ -n "$cert_file" ]] || die "Custom cert provider requires --cert"
    [[ -n "$key_file" ]] || die "Custom cert provider requires --key"
    [[ -f "$cert_file" ]] || die "Certificate file not found: $cert_file"
    [[ -f "$key_file" ]] || die "Private key file not found: $key_file"
    if ! cert_covers_fqdn "$cert_file" "$fqdn"; then
      die "Selected certificate does not cover ${fqdn}."
    fi
  fi

  if [[ "$selected_provider" == "auto" ]]; then
    local looks_like_pixellar=0
    if [[ "$fqdn" == "$godaddy_domain" || "$fqdn" == *".${godaddy_domain}" ]]; then
      looks_like_pixellar=1
    fi

    if [[ "$looks_like_pixellar" -eq 1 ]]; then
      local auto_cert=""
      local auto_key=""
      if pick_godaddy_pair "$godaddy_domain" "$search_dirs" "$cert_index" "$((1-non_interactive))" auto_cert auto_key; then
        if cert_covers_fqdn "$auto_cert" "$fqdn"; then
          cert_file="$auto_cert"
          key_file="$auto_key"
          selected_provider="godaddy"
          log "Auto-selected GoDaddy cert for ${fqdn}"
        else
          warn "Discovered GoDaddy cert does not cover ${fqdn}; falling back to LetsEncrypt"
          selected_provider="letsencrypt"
        fi
      else
        warn "No local GoDaddy cert found; falling back to LetsEncrypt"
        selected_provider="letsencrypt"
      fi
    else
      if fqdn_looks_local_dev "$fqdn"; then
        selected_provider="mkcert"
      else
        selected_provider="letsencrypt"
      fi
    fi
  fi

  if [[ "$selected_provider" == "godaddy" && ( -z "$cert_file" || -z "$key_file" ) ]]; then
    if ! pick_godaddy_pair "$godaddy_domain" "$search_dirs" "$cert_index" "$((1-non_interactive))" cert_file key_file; then
      die "No matching GoDaddy cert/key found for ${godaddy_domain}."
    fi
  fi

  if [[ "$selected_provider" == "godaddy" ]]; then
    [[ -f "$cert_file" ]] || die "Certificate file not found: $cert_file"
    [[ -f "$key_file" ]] || die "Private key file not found: $key_file"

    if ! cert_covers_fqdn "$cert_file" "$fqdn"; then
      die "Selected certificate does not cover ${fqdn}."
    fi
  fi

  if [[ "$selected_provider" == "mkcert" ]]; then
    if [[ -z "$cert_file" ]]; then
      cert_file="${DEFAULT_MKCERT_DIR}/${fqdn}/fullchain.pem"
    fi
    if [[ -z "$key_file" ]]; then
      key_file="${DEFAULT_MKCERT_DIR}/${fqdn}/privkey.pem"
    fi
    issue_mkcert_cert "$fqdn" "$cert_file" "$key_file"
  fi

  local vhost_file
  vhost_file="$(get_vhost_file "$server" "$fqdn")"
  if [[ "$server" == "apache" ]]; then
    ensure_apache_include_dir "$(dirname "$vhost_file")"
  fi

  ensure_can_overwrite_vhost "$vhost_file" "$force" "$((1-non_interactive))"
  backup_if_exists "$vhost_file"

  if [[ "$selected_provider" == "letsencrypt" ]]; then
    if [[ "$non_interactive" -eq 0 && -z "$email" ]]; then
      ask_text email "LetsEncrypt email" "$email"
    fi

    validate_dns_for_letsencrypt "$fqdn"

    if [[ "$server" == "apache" ]]; then
      enable_apache_modules_if_possible "$mode"
      write_apache_challenge_vhost "$vhost_file" "$fqdn" "$acme_webroot"
      enable_apache_site_if_needed "$vhost_file"
    else
      write_nginx_challenge_vhost "$vhost_file" "$fqdn" "$acme_webroot"
    fi

    server_reload "$server"

    issue_letsencrypt_cert "$fqdn" "$email" "$acme_webroot"

    cert_file="${DEFAULT_LE_LIVE_DIR}/${fqdn}/fullchain.pem"
    key_file="${DEFAULT_LE_LIVE_DIR}/${fqdn}/privkey.pem"

    [[ -f "$cert_file" ]] || die "LetsEncrypt cert not found at ${cert_file}"
    [[ -f "$key_file" ]] || die "LetsEncrypt key not found at ${key_file}"
  fi

  if [[ "$server" == "apache" ]]; then
    enable_apache_modules_if_possible "$mode"
    write_apache_final_vhost "$vhost_file" "$fqdn" "$mode" "$upstream" "$document_root" "$cert_file" "$key_file" "$acme_webroot"
    enable_apache_site_if_needed "$vhost_file"
  else
    write_nginx_final_vhost "$vhost_file" "$fqdn" "$mode" "$upstream" "$document_root" "$cert_file" "$key_file" "$acme_webroot"
  fi

  server_reload "$server"

  if [[ "$selected_provider" == "mkcert" ]] || fqdn_looks_local_dev "$fqdn"; then
    ensure_managed_hosts_entry "$fqdn" "$DEFAULT_LOCAL_HOSTS_IP"
  fi

  LAST_CREATED_FQDN="$fqdn"

  log "Site ready: https://${fqdn}"
  log "Server: ${server}"
  log "Mode: ${mode}"
  log "Cert provider used: ${selected_provider}"
  log "Vhost file: ${vhost_file}"
  if [[ "$mode" == "static" ]]; then
    log "DocumentRoot: ${document_root}"
  else
    log "Upstream: ${upstream}"
  fi

  if [[ "$selected_provider" == "mkcert" ]]; then
    log "Local TLS note: ${fqdn} se gestiona automaticamente en ${DEFAULT_HOSTS_FILE} (IP ${DEFAULT_LOCAL_HOSTS_IP})."
  elif [[ "$fqdn" == *".${godaddy_domain}" || "$fqdn" == "$godaddy_domain" ]]; then
    log "DNS reminder: ensure wildcard A record exists: *.${godaddy_domain} -> your EC2 public IP"
  else
    log "DNS reminder: ensure A/AAAA for ${fqdn} points to this server for LetsEncrypt renewals"
  fi
}

guess_provider_from_map() {
  local cert_file="$1"
  local key_file="$2"
  local pair_map_file="$3"

  if [[ -z "$cert_file" || -z "$key_file" ]]; then
    echo "none"
    return 0
  fi

  if [[ "$cert_file" == /etc/letsencrypt/live/* ]]; then
    echo "letsencrypt"
    return 0
  fi

  if [[ "$cert_file" == "${DEFAULT_MKCERT_DIR}/"* ]]; then
    echo "mkcert"
    return 0
  fi

  if cert_issuer_is_mkcert "$cert_file"; then
    echo "mkcert"
    return 0
  fi

  if [[ -f "$pair_map_file" ]]; then
    if grep -Fqx "${cert_file}"$'\t'"${key_file}" "$pair_map_file"; then
      echo "godaddy"
      return 0
    fi
  fi

  echo "custom"
}

collect_apache_inventory_entries() {
  local pair_map_file="$1"
  local apache_dirs=()
  read_lines_to_array apache_dirs < <(detect_apache_conf_dirs)
  [[ "${#apache_dirs[@]}" -gt 0 ]] || return 0

  local seen_files=()
  local row_keys=()
  local row_values=()
  local row_tls=()
  local dir file real_file dup s
  local fqdn cert key docroot upstream mode has_tls provider
  local row key idx existing_tls

  for dir in "${apache_dirs[@]-}"; do
    [[ -d "$dir" ]] || continue
    for file in "$dir"/*; do
      is_vhost_candidate_file "$file" || continue

      real_file="$(cd "$(dirname "$file")" 2>/dev/null && pwd -P)/$(basename "$file")"
      dup=0
      for s in "${seen_files[@]-}"; do
        if [[ "$s" == "$real_file" ]]; then
          dup=1
          break
        fi
      done
      if [[ "$dup" -eq 1 ]]; then
        continue
      fi
      seen_files+=("$real_file")

      fqdn="$(awk 'tolower($1)=="servername"{print $2; exit}' "$file" 2>/dev/null || true)"
      if [[ -z "$fqdn" ]]; then
        fqdn="$(awk '
          BEGIN { IGNORECASE=1 }
          tolower($1)=="serveralias" {
            for (i=2; i<=NF; i++) {
              gsub(/;$/,"",$i)
              if ($i != "_" && $i != "*" && $i !~ /\*/) { print $i; exit }
            }
          }
        ' "$file" 2>/dev/null || true)"
      fi
      fqdn="$(clean_conf_value "$fqdn")"
      [[ -n "$fqdn" ]] || continue

      cert="$(awk 'tolower($1)=="sslcertificatefile"{print $2; exit}' "$file" 2>/dev/null || true)"
      key="$(awk 'tolower($1)=="sslcertificatekeyfile"{print $2; exit}' "$file" 2>/dev/null || true)"
      docroot="$(awk 'tolower($1)=="documentroot"{print $2; exit}' "$file" 2>/dev/null || true)"
      cert="$(clean_conf_value "$cert")"
      key="$(clean_conf_value "$key")"
      docroot="$(clean_conf_value "$docroot")"

      mode="static"
      upstream=""
      if grep -Eiq '^[[:space:]]*ProxyPass[[:space:]]+' "$file"; then
        mode="proxy"
        upstream="$(awk 'tolower($1)=="proxypass"{print $3; exit}' "$file" 2>/dev/null || true)"
        upstream="$(clean_conf_value "$upstream")"
      fi

      has_tls="0"
      [[ -n "$cert" && -n "$key" ]] && has_tls="1"
      provider="$(guess_provider_from_map "$cert" "$key" "$pair_map_file")"

      row="$(printf "%s\x1fapache\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s" \
        "$fqdn" "$mode" "$has_tls" "$provider" "$cert" "$key" "$docroot" "$upstream" "$file")"
      key="apache|${fqdn}"

      idx=-1
      local i
      for ((i=0; i<${#row_keys[@]}; i++)); do
        if [[ "${row_keys[$i]}" == "$key" ]]; then
          idx="$i"
          break
        fi
      done

      if (( idx < 0 )); then
        row_keys+=("$key")
        row_values+=("$row")
        row_tls+=("$has_tls")
      else
        existing_tls="${row_tls[$idx]}"
        if [[ "$existing_tls" != "1" && "$has_tls" == "1" ]]; then
          row_values[$idx]="$row"
          row_tls[$idx]="$has_tls"
        fi
      fi
    done
  done

  local out_row
  for out_row in "${row_values[@]-}"; do
    [[ -n "$out_row" ]] || continue
    printf "%s\n" "$out_row"
  done
}

collect_nginx_inventory_entries() {
  local pair_map_file="$1"
  local dir="$DEFAULT_NGINX_CONF_DIR"
  [[ -d "$dir" ]] || return 0

  local file fqdn cert key docroot upstream mode has_tls provider
  for file in "$dir"/*; do
    is_vhost_candidate_file "$file" || continue

    fqdn="$(awk '$1=="server_name"{for(i=2;i<=NF;i++){gsub(/;$/,"",$i); if($i!="_"){print $i; exit}}}' "$file" 2>/dev/null || true)"
    fqdn="$(clean_conf_value "$fqdn")"
    [[ -n "$fqdn" ]] || continue

    cert="$(awk '$1=="ssl_certificate"{print $2; exit}' "$file" 2>/dev/null || true)"
    key="$(awk '$1=="ssl_certificate_key"{print $2; exit}' "$file" 2>/dev/null || true)"
    docroot="$(awk '$1=="root"{print $2; exit}' "$file" 2>/dev/null || true)"
    cert="$(clean_conf_value "$cert")"
    key="$(clean_conf_value "$key")"
    docroot="$(clean_conf_value "$docroot")"

    mode="static"
    upstream=""
    if grep -Eiq '^[[:space:]]*proxy_pass[[:space:]]+' "$file"; then
      mode="proxy"
      upstream="$(awk '$1=="proxy_pass"{print $2; exit}' "$file" 2>/dev/null || true)"
      upstream="$(clean_conf_value "$upstream")"
    fi

    has_tls="0"
    [[ -n "$cert" && -n "$key" ]] && has_tls="1"
    provider="$(guess_provider_from_map "$cert" "$key" "$pair_map_file")"

    printf "%s\x1fnginx\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\n" \
      "$fqdn" "$mode" "$has_tls" "$provider" "$cert" "$key" "$docroot" "$upstream" "$file"
  done
}

migrate_inventory() {
  local output=""
  local godaddy_domain="$DEFAULT_GODADDY_DOMAIN"
  local search_dirs="$DEFAULT_GODADDY_SEARCH_DIRS"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --output) output="${2:-}"; shift 2 ;;
      --godaddy-domain) godaddy_domain="${2:-}"; shift 2 ;;
      --search-dirs) search_dirs="${2:-}"; shift 2 ;;
      --help|-h)
        print_usage
        exit 0
        ;;
      *)
        die "Unknown migrate option: $1"
        ;;
    esac
  done

  godaddy_domain="$(normalize_lower "$godaddy_domain")"
  [[ "$godaddy_domain" =~ ^[A-Za-z0-9.-]+$ ]] || die "Invalid --godaddy-domain"

  if [[ -z "$output" ]]; then
    output="/var/tmp/ssl-admin-migrate-$(date +%Y%m%d-%H%M%S).json"
  fi

  mkdir -p "$(dirname "$output")"

  need_cmd openssl

  local pair_map_file
  pair_map_file="$(mktemp)"
  local pair_map_cleanup=1
  trap '[[ "${pair_map_cleanup:-0}" -eq 1 ]] && rm -f "$pair_map_file"' RETURN

  local godaddy_pairs=()
  read_lines_to_array godaddy_pairs < <(collect_matching_cert_pairs "$godaddy_domain" "$search_dirs")

  local pair
  if [[ "${#godaddy_pairs[@]}" -gt 0 ]]; then
    for pair in "${godaddy_pairs[@]}"; do
      [[ -n "$pair" ]] || continue
      printf "%s\n" "$pair" >> "$pair_map_file"
    done
  fi

  local apache_entries=()
  local nginx_entries=()
  read_lines_to_array apache_entries < <(collect_apache_inventory_entries "$pair_map_file")
  read_lines_to_array nginx_entries < <(collect_nginx_inventory_entries "$pair_map_file")

  local entries=()
  local row
  if [[ "${#apache_entries[@]}" -gt 0 ]]; then
    for row in "${apache_entries[@]}"; do
      [[ -n "$row" ]] || continue
      entries+=("$row")
    done
  fi
  if [[ "${#nginx_entries[@]}" -gt 0 ]]; then
    for row in "${nginx_entries[@]}"; do
      [[ -n "$row" ]] || continue
      entries+=("$row")
    done
  fi

  local le_certs=()
  if [[ -d "$DEFAULT_LE_LIVE_DIR" ]]; then
    local d n
    for d in "$DEFAULT_LE_LIVE_DIR"/*; do
      [[ -d "$d" ]] || continue
      n="$(basename "$d")"
      [[ "$n" == "README" ]] && continue
      le_certs+=("$n")
    done
  fi

  {
    printf "{\n"
    printf "  \"generated_at\": \"%s\",\n" "$(json_escape "$(date -u +%Y-%m-%dT%H:%M:%SZ)")"
    printf "  \"host\": \"%s\",\n" "$(json_escape "$(hostname 2>/dev/null || echo unknown)")"
    printf "  \"godaddy_domain\": \"%s\",\n" "$(json_escape "$godaddy_domain")"
    printf "  \"output_version\": 1,\n"

    printf "  \"godaddy_pairs\": [\n"
    local first=1
    local cert key
    if [[ "${#godaddy_pairs[@]}" -gt 0 ]]; then
      for pair in "${godaddy_pairs[@]}"; do
        [[ -n "$pair" ]] || continue
        IFS=$'\t' read -r cert key <<< "$pair"
        if [[ "$first" -eq 0 ]]; then printf ",\n"; fi
        first=0
        printf "    {\"cert\": \"%s\", \"key\": \"%s\"}" "$(json_escape "$cert")" "$(json_escape "$key")"
      done
    fi
    printf "\n  ],\n"

    printf "  \"letsencrypt_certs\": [\n"
    first=1
    local le
    if [[ "${#le_certs[@]}" -gt 0 ]]; then
      for le in "${le_certs[@]}"; do
        [[ -n "$le" ]] || continue
        if [[ "$first" -eq 0 ]]; then printf ",\n"; fi
        first=0
        printf "    \"%s\"" "$(json_escape "$le")"
      done
    fi
    printf "\n  ],\n"

    printf "  \"vhosts\": [\n"
    first=1
    local entry fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file has_tls_bool
    if [[ "${#entries[@]}" -gt 0 ]]; then
      for entry in "${entries[@]}"; do
        [[ -n "$entry" ]] || continue
        IFS=$'\x1f' read -r fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file <<< "$entry"
        [[ -n "$fqdn" ]] || continue
        if [[ "$first" -eq 0 ]]; then printf ",\n"; fi
        first=0
        has_tls_bool="false"
        [[ "$has_tls" == "1" ]] && has_tls_bool="true"
        printf "    {\"fqdn\": \"%s\", \"server\": \"%s\", \"mode\": \"%s\", \"has_tls\": %s, \"provider_guess\": \"%s\", \"cert_file\": \"%s\", \"key_file\": \"%s\", \"document_root\": \"%s\", \"upstream\": \"%s\", \"vhost_file\": \"%s\"}" \
          "$(json_escape "$fqdn")" \
          "$(json_escape "$server")" \
          "$(json_escape "$mode")" \
          "$has_tls_bool" \
          "$(json_escape "$provider")" \
          "$(json_escape "$cert_file")" \
          "$(json_escape "$key_file")" \
          "$(json_escape "$docroot")" \
          "$(json_escape "$upstream")" \
          "$(json_escape "$vhost_file")"
      done
    fi
    printf "\n  ]\n"
    printf "}\n"
  } > "$output"

  rm -f "$pair_map_file"
  pair_map_cleanup=0
  trap - RETURN

  log "Migration inventory generated: $output"
  log "Vhosts found: ${#entries[@]}"
  log "LetsEncrypt certs found: ${#le_certs[@]}"
  log "GoDaddy cert/key pairs found: ${#godaddy_pairs[@]}"
}

to_epoch() {
  local date_str="$1"
  if date -u -d "$date_str" +%s >/dev/null 2>&1; then
    date -u -d "$date_str" +%s
    return 0
  fi
  if date -j -u -f "%b %e %T %Y %Z" "$date_str" +%s >/dev/null 2>&1; then
    date -j -u -f "%b %e %T %Y %Z" "$date_str" +%s
    return 0
  fi
  return 1
}

cert_expiry_raw() {
  local cert_file="$1"
  openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | sed 's/^notAfter=//'
}

cert_days_left() {
  local cert_file="$1"
  local now_epoch="$2"
  local raw end_epoch

  raw="$(cert_expiry_raw "$cert_file")"
  [[ -n "$raw" ]] || return 1
  end_epoch="$(to_epoch "$raw")" || return 1

  if (( end_epoch >= now_epoch )); then
    echo $(( (end_epoch - now_epoch + 86399) / 86400 ))
  else
    echo $(( -1 * ((now_epoch - end_epoch + 86399) / 86400) ))
  fi
}

collect_all_inventory_entries() {
  local godaddy_domain="$1"
  local search_dirs="$2"

  local pair_map_file
  pair_map_file="$(mktemp)"

  local pairs=()
  read_lines_to_array pairs < <(collect_matching_cert_pairs "$godaddy_domain" "$search_dirs")
  if [[ "${#pairs[@]}" -gt 0 ]]; then
    local p
    for p in "${pairs[@]}"; do
      [[ -n "$p" ]] || continue
      printf "%s\n" "$p" >> "$pair_map_file"
    done
  fi

  collect_apache_inventory_entries "$pair_map_file"
  collect_nginx_inventory_entries "$pair_map_file"

  rm -f "$pair_map_file"
}

compute_ssl_state() {
  local cert_file="$1"
  local provider="$2"
  local warn_days="$3"
  local now_epoch="$4"
  local __state_var="$5"
  local __days_var="$6"
  local __expires_var="$7"
  local __renew_var="$8"

  local out_state="NO_CERT"
  local out_days="-"
  local out_expires_at="-"
  local out_renew="no"

  if [[ "$provider" == "letsencrypt" ]]; then
    out_renew="auto"
  elif [[ "$provider" == "godaddy" || "$provider" == "custom" ]]; then
    out_renew="manual"
  elif [[ "$provider" == "mkcert" ]]; then
    out_renew="script"
  fi

  if [[ -n "$cert_file" && -f "$cert_file" ]]; then
    out_expires_at="$(cert_expiry_raw "$cert_file" || true)"
    if [[ -n "$out_expires_at" ]]; then
      out_days="$(cert_days_left "$cert_file" "$now_epoch" || echo "?")"
      if [[ "$out_days" =~ ^-?[0-9]+$ ]]; then
        if (( out_days < 0 )); then
          out_state="EXPIRED"
        elif (( out_days <= warn_days )); then
          out_state="EXPIRING"
        else
          out_state="OK"
        fi
      else
        out_state="UNKNOWN"
      fi
    else
      out_state="INVALID"
    fi
  fi

  printf -v "$__state_var" "%s" "$out_state"
  printf -v "$__days_var" "%s" "$out_days"
  printf -v "$__expires_var" "%s" "$out_expires_at"
  printf -v "$__renew_var" "%s" "$out_renew"
}

dashboard_ssl() {
  local warn_days=30
  local godaddy_domain="$DEFAULT_GODADDY_DOMAIN"
  local search_dirs="$DEFAULT_GODADDY_SEARCH_DIRS"
  local json_mode=0
  local output_file=""

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --warn-days) warn_days="${2:-}"; shift 2 ;;
      --godaddy-domain) godaddy_domain="${2:-}"; shift 2 ;;
      --search-dirs) search_dirs="${2:-}"; shift 2 ;;
      --json) json_mode=1; shift ;;
      --output) output_file="${2:-}"; shift 2 ;;
      --help|-h)
        print_usage
        exit 0
        ;;
      *)
        die "Unknown dashboard option: $1"
        ;;
    esac
  done

  [[ "$warn_days" =~ ^[0-9]+$ ]] || die "--warn-days must be a positive integer"
  (( warn_days >= 1 )) || die "--warn-days must be >= 1"
  if [[ -n "$output_file" && "$json_mode" -ne 1 ]]; then
    die "--output can only be used together with --json"
  fi
  need_cmd openssl

  local entries=()
  read_lines_to_array entries < <(collect_all_inventory_entries "$godaddy_domain" "$search_dirs")

  local now_epoch
  now_epoch="$(date +%s)"

  local total=0
  local ok=0
  local expiring=0
  local expired=0
  local no_cert=0
  local invalid=0
  local unknown=0
  local le_due=()
  local mk_due=()
  local gd_due=()
  local detail_entries=()

  local e fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file
  local state days expires_at renew

  if [[ "${#entries[@]}" -gt 0 ]]; then
    for e in "${entries[@]}"; do
      [[ -n "$e" ]] || continue
      IFS=$'\x1f' read -r fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file <<< "$e"
      [[ -n "$fqdn" ]] || continue

      total=$((total + 1))
      compute_ssl_state "$cert_file" "$provider" "$warn_days" "$now_epoch" state days expires_at renew

      case "$state" in
        OK) ok=$((ok + 1)) ;;
        EXPIRING) expiring=$((expiring + 1)) ;;
        EXPIRED) expired=$((expired + 1)) ;;
        NO_CERT) no_cert=$((no_cert + 1)) ;;
        INVALID) invalid=$((invalid + 1)) ;;
        UNKNOWN) unknown=$((unknown + 1)) ;;
      esac

      if [[ "$state" == "EXPIRING" || "$state" == "EXPIRED" ]]; then
        detail_entries+=("${fqdn}"$'\x1f'"${server}"$'\x1f'"${provider}"$'\x1f'"${state}"$'\x1f'"${days}"$'\x1f'"${expires_at}")
        if [[ "$provider" == "letsencrypt" ]]; then
          le_due+=("$fqdn")
        elif [[ "$provider" == "mkcert" ]]; then
          mk_due+=("$fqdn")
        elif [[ "$provider" == "godaddy" || "$provider" == "custom" ]]; then
          gd_due+=("$fqdn")
        fi
      fi
    done
  fi

  if [[ "$json_mode" -eq 1 ]]; then
    local generated_at
    generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    local host_name
    host_name="$(hostname 2>/dev/null || echo unknown)"
    local renew_cmd
    renew_cmd="sudo ./ssl-admin.sh renew --warn-days ${warn_days}"

    local json_payload
    json_payload="$(mktemp)"
    {
      printf "{\n"
      printf "  \"generated_at\": \"%s\",\n" "$(json_escape "$generated_at")"
      printf "  \"host\": \"%s\",\n" "$(json_escape "$host_name")"
      printf "  \"warn_days\": %s,\n" "$warn_days"
      printf "  \"summary\": {\n"
      printf "    \"total\": %s,\n" "$total"
      printf "    \"ok\": %s,\n" "$ok"
      printf "    \"expiring\": %s,\n" "$expiring"
      printf "    \"expired\": %s,\n" "$expired"
      printf "    \"no_cert\": %s,\n" "$no_cert"
      printf "    \"invalid\": %s,\n" "$invalid"
      printf "    \"unknown\": %s\n" "$unknown"
      printf "  },\n"
      printf "  \"recommendation\": {\n"
      if (( expired > 0 )); then
        printf "    \"immediate_action_required\": true,\n"
      else
        printf "    \"immediate_action_required\": false,\n"
      fi
      if [[ "${#le_due[@]}" -gt 0 ]]; then
        printf "    \"renew_letsencrypt\": true,\n"
        printf "    \"renew_command\": \"%s\",\n" "$(json_escape "$renew_cmd")"
      else
        printf "    \"renew_letsencrypt\": false,\n"
        printf "    \"renew_command\": \"\",\n"
      fi
      if [[ "${#mk_due[@]}" -gt 0 ]]; then
        printf "    \"renew_mkcert\": true,\n"
        printf "    \"renew_mkcert_command\": \"%s\",\n" "$(json_escape "sudo ./ssl-admin.sh renew --warn-days ${warn_days} --include-mkcert")"
      else
        printf "    \"renew_mkcert\": false,\n"
        printf "    \"renew_mkcert_command\": \"\",\n"
      fi
      printf "    \"manual_review\": [\n"
      local first=1
      local gd
      if [[ "${#gd_due[@]}" -gt 0 ]]; then
        for gd in "${gd_due[@]}"; do
          [[ -n "$gd" ]] || continue
          if [[ "$first" -eq 0 ]]; then printf ",\n"; fi
          first=0
          printf "      \"%s\"" "$(json_escape "$gd")"
        done
      fi
      printf "\n    ]\n"
      printf "  },\n"
      printf "  \"expiring_or_expired\": [\n"
      first=1
      local de dfqdn dserver dprovider dstate ddays dexpires
      if [[ "${#detail_entries[@]}" -gt 0 ]]; then
        for de in "${detail_entries[@]}"; do
          [[ -n "$de" ]] || continue
          IFS=$'\x1f' read -r dfqdn dserver dprovider dstate ddays dexpires <<< "$de"
          if [[ "$first" -eq 0 ]]; then printf ",\n"; fi
          first=0
          printf "    {\"fqdn\": \"%s\", \"server\": \"%s\", \"provider\": \"%s\", \"state\": \"%s\", \"days_left\": \"%s\", \"expires_at\": \"%s\"}" \
            "$(json_escape "$dfqdn")" \
            "$(json_escape "$dserver")" \
            "$(json_escape "$dprovider")" \
            "$(json_escape "$dstate")" \
            "$(json_escape "$ddays")" \
            "$(json_escape "$dexpires")"
        done
      fi
      printf "\n  ]\n"
      printf "}\n"
    } > "$json_payload"

    if [[ -n "$output_file" ]]; then
      mkdir -p "$(dirname "$output_file")"
      cp "$json_payload" "$output_file"
      rm -f "$json_payload"
      log "Dashboard JSON written to: $output_file"
    else
      cat "$json_payload"
      rm -f "$json_payload"
    fi
    return 0
  fi

  echo "SSL Dashboard"
  echo "Window: ${warn_days} day(s)"
  echo "Generated: $(date '+%Y-%m-%d %H:%M:%S %Z')"
  echo
  echo "Summary"
  echo "  Total SSL entries: $total"
  echo "  $(ssl_state_colored_paren "OK"): $ok"
  echo "  $(ssl_state_colored_paren "EXPIRING"): $expiring"
  echo "  $(ssl_state_colored_paren "EXPIRED"): $expired"
  echo "  $(ssl_state_colored_paren "NO_CERT"): $no_cert"
  echo "  $(ssl_state_colored_paren "INVALID"): $invalid"
  echo "  $(ssl_state_colored_paren "UNKNOWN"): $unknown"
  echo

  if [[ "${#detail_entries[@]}" -gt 0 ]]; then
    printf "%-35s %-7s %-11s %-10s %-9s %-25s\n" "FQDN" "SERVER" "PROVIDER" "STATE" "DAYS" "EXPIRES_AT"
    printf "%-35s %-7s %-11s %-10s %-9s %-25s\n" "-----------------------------------" "-------" "-----------" "----------" "---------" "-------------------------"
    local de dfqdn dserver dprovider dstate ddays dexpires
    local dstate_fmt
    for de in "${detail_entries[@]}"; do
      [[ -n "$de" ]] || continue
      IFS=$'\x1f' read -r dfqdn dserver dprovider dstate ddays dexpires <<< "$de"
      dstate_fmt="$(ssl_state_colored_paren "$dstate")"
      printf "%-35s %-7s %-11s %s %-9s %-25s\n" "$dfqdn" "$dserver" "$dprovider" "$dstate_fmt" "$ddays" "$dexpires"
    done
    echo
  fi

  echo "Recommendation"
  if (( expired > 0 )); then
    echo "  Immediate action required: there are expired certificates."
  fi
  if [[ "${#le_due[@]}" -gt 0 ]]; then
    echo "  Run automatic renewal for LetsEncrypt:"
    echo "    sudo ./ssl-admin.sh renew --warn-days ${warn_days}"
  fi
  if [[ "${#mk_due[@]}" -gt 0 ]]; then
    echo "  Run mkcert regeneration for local certificates:"
    echo "    sudo ./ssl-admin.sh renew --warn-days ${warn_days} --include-mkcert"
  fi
  if [[ "${#gd_due[@]}" -gt 0 ]]; then
    echo "  Review manual certificates (GoDaddy/custom) for:"
    local g
    for g in "${gd_due[@]}"; do
      echo "    - $g"
    done
    echo "  Then update cert/key via create workflow."
  fi
  if (( expiring == 0 && expired == 0 )); then
    echo "  No renewal action required right now."
  fi
}

status_ssl() {
  local warn_days=30
  local fqdn_filter=""
  local godaddy_domain="$DEFAULT_GODADDY_DOMAIN"
  local search_dirs="$DEFAULT_GODADDY_SEARCH_DIRS"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --warn-days) warn_days="${2:-}"; shift 2 ;;
      --fqdn) fqdn_filter="${2:-}"; shift 2 ;;
      --godaddy-domain) godaddy_domain="${2:-}"; shift 2 ;;
      --search-dirs) search_dirs="${2:-}"; shift 2 ;;
      --help|-h)
        print_usage
        exit 0
        ;;
      *)
        die "Unknown status option: $1"
        ;;
    esac
  done

  [[ "$warn_days" =~ ^[0-9]+$ ]] || die "--warn-days must be a positive integer"
  (( warn_days >= 1 )) || die "--warn-days must be >= 1"

  need_cmd openssl

  local entries=()
  read_lines_to_array entries < <(collect_all_inventory_entries "$godaddy_domain" "$search_dirs")

  local now_epoch
  now_epoch="$(date +%s)"

  printf "%-35s %-7s %-11s %-10s %-9s %-10s %-25s %s\n" "FQDN" "SERVER" "PROVIDER" "STATE" "DAYS" "RENEW" "EXPIRES_AT" "CERT_FILE"
  printf "%-35s %-7s %-11s %-10s %-9s %-10s %-25s %s\n" "-----------------------------------" "-------" "-----------" "----------" "---------" "----------" "-------------------------" "---------"

  local shown=0
  local e fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file
  local days state renew expires_at state_fmt

  if [[ "${#entries[@]}" -gt 0 ]]; then
    for e in "${entries[@]}"; do
      [[ -n "$e" ]] || continue
      IFS=$'\x1f' read -r fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file <<< "$e"
      [[ -n "$fqdn" ]] || continue

      if [[ -n "$fqdn_filter" && "$fqdn" != "$fqdn_filter" ]]; then
        continue
      fi

      compute_ssl_state "$cert_file" "$provider" "$warn_days" "$now_epoch" state days expires_at renew

      shown=$((shown + 1))
      state_fmt="$(ssl_state_colored_paren "$state")"
      printf "%-35s %-7s %-11s %s %-9s %-10s %-25s %s\n" \
        "$fqdn" "$server" "$provider" "$state_fmt" "$days" "$renew" "$expires_at" "${cert_file:--}"
    done
  fi

  if [[ "$shown" -eq 0 ]]; then
    echo "(no matching SSL entries found)"
  fi
}

reload_services_after_renew() {
  local reloaded=0

  if command -v nginx >/dev/null 2>&1; then
    if [[ -d "$DEFAULT_NGINX_CONF_DIR" ]]; then
      test_and_reload_nginx || warn "Could not reload Nginx after renew."
      reloaded=1
    fi
  fi

  if command -v apache2ctl >/dev/null 2>&1 || command -v apachectl >/dev/null 2>&1 || command -v httpd >/dev/null 2>&1; then
    local apache_dir
    apache_dir="$(detect_apache_conf_dir)"
    if [[ -d "$apache_dir" ]]; then
      test_and_reload_apache || warn "Could not reload Apache after renew."
      reloaded=1
    fi
  fi

  if [[ "$reloaded" -eq 1 ]]; then
    log "Web services reloaded after certificate renewal."
  else
    warn "No web service was reloaded (Apache/Nginx not detected)."
  fi
}

collect_due_mkcert_entries() {
  local warn_days="$1"
  local godaddy_domain="$2"
  local search_dirs="$3"
  shift 3
  local filters=("$@")

  local entries=()
  read_lines_to_array entries < <(collect_all_inventory_entries "$godaddy_domain" "$search_dirs")

  local now_epoch
  now_epoch="$(date +%s)"

  local out=()
  local e fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file
  local days row exists existing f matched

  for e in "${entries[@]-}"; do
    [[ -n "$e" ]] || continue
    IFS=$'\x1f' read -r fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file <<< "$e"
    [[ "$provider" == "mkcert" ]] || continue
    [[ -n "$fqdn" ]] || continue
    [[ -n "$cert_file" && -n "$key_file" ]] || continue

    if [[ "${#filters[@]}" -gt 0 ]]; then
      matched=0
      for f in "${filters[@]}"; do
        if [[ "$fqdn" == "$f" ]]; then
          matched=1
          break
        fi
      done
      [[ "$matched" -eq 1 ]] || continue
    fi

    days="$(cert_days_left "$cert_file" "$now_epoch" || echo "?")"
    if [[ "$days" =~ ^-?[0-9]+$ ]] && (( days <= warn_days )); then
      row="${fqdn}"$'\t'"${cert_file}"$'\t'"${key_file}"
      exists=0
      for existing in "${out[@]}"; do
        if [[ "$existing" == "$row" ]]; then
          exists=1
          break
        fi
      done
      if [[ "$exists" -eq 0 ]]; then
        out+=("$row")
      fi
    fi
  done

  for row in "${out[@]}"; do
    [[ -n "$row" ]] || continue
    printf "%s\n" "$row"
  done
}

renew_ssl() {
  local warn_days=30
  local fqdn_filters=()
  local dry_run=0
  local force_renewal=0
  local no_reload=0
  local include_mkcert=0
  local mkcert_only=0
  local godaddy_domain="$DEFAULT_GODADDY_DOMAIN"
  local search_dirs="$DEFAULT_GODADDY_SEARCH_DIRS"
  local renew_le=1
  local renew_mk=0

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --warn-days) warn_days="${2:-}"; shift 2 ;;
      --fqdn) fqdn_filters+=("${2:-}"); shift 2 ;;
      --dry-run) dry_run=1; shift ;;
      --force-renewal) force_renewal=1; shift ;;
      --include-mkcert) include_mkcert=1; shift ;;
      --mkcert-only) mkcert_only=1; shift ;;
      --godaddy-domain) godaddy_domain="${2:-}"; shift 2 ;;
      --search-dirs) search_dirs="${2:-}"; shift 2 ;;
      --no-reload) no_reload=1; shift ;;
      --help|-h)
        print_usage
        exit 0
        ;;
      *)
        die "Unknown renew option: $1"
        ;;
    esac
  done

  [[ "$warn_days" =~ ^[0-9]+$ ]] || die "--warn-days must be a positive integer"
  (( warn_days >= 1 )) || die "--warn-days must be >= 1"

  if [[ "$mkcert_only" -eq 1 ]]; then
    include_mkcert=1
    renew_le=0
  fi
  if [[ "$include_mkcert" -eq 1 ]]; then
    renew_mk=1
  fi

  require_root
  need_cmd openssl

  if [[ "$renew_le" -eq 1 ]]; then
    if ! command -v certbot >/dev/null 2>&1; then
      if [[ "$renew_mk" -eq 1 ]]; then
        warn "certbot not found; skipping LetsEncrypt renewals."
        renew_le=0
      else
        die "Missing required command: certbot"
      fi
    elif [[ ! -d "$DEFAULT_LE_LIVE_DIR" ]]; then
      if [[ "$renew_mk" -eq 1 ]]; then
        warn "LetsEncrypt live directory not found (${DEFAULT_LE_LIVE_DIR}); skipping LetsEncrypt renewals."
        renew_le=0
      else
        die "LetsEncrypt live directory not found: $DEFAULT_LE_LIVE_DIR"
      fi
    fi
  fi

  if [[ "$renew_mk" -eq 1 ]]; then
    if ! command -v mkcert >/dev/null 2>&1; then
      if [[ "$renew_le" -eq 1 ]]; then
        warn "mkcert not found; skipping mkcert renewals."
        renew_mk=0
      else
        die "Missing required command: mkcert"
      fi
    fi
  fi

  if [[ "$renew_le" -eq 0 && "$renew_mk" -eq 0 ]]; then
    die "No renewal provider available. Install certbot and/or mkcert."
  fi

  local names=()
  local le_due=()
  local mk_due_entries=()
  local d n matched ff
  local now_epoch
  now_epoch="$(date +%s)"
  local name cert days

  if [[ "$renew_le" -eq 1 ]]; then
    for d in "$DEFAULT_LE_LIVE_DIR"/*; do
      [[ -d "$d" ]] || continue
      n="$(basename "$d")"
      [[ "$n" == "README" ]] && continue
      if [[ "${#fqdn_filters[@]}" -gt 0 ]]; then
        matched=0
        for ff in "${fqdn_filters[@]}"; do
          if [[ "$n" == "$ff" ]]; then
            matched=1
            break
          fi
        done
        [[ "$matched" -eq 1 ]] || continue
      fi
      names+=("$n")
    done

    if [[ "${#names[@]}" -eq 0 ]]; then
      if [[ "$renew_mk" -eq 0 ]]; then
        die "No LetsEncrypt certificates found for requested filter."
      fi
      warn "No LetsEncrypt certificates found for requested filter."
    else
      for name in "${names[@]}"; do
        cert="${DEFAULT_LE_LIVE_DIR}/${name}/fullchain.pem"
        [[ -f "$cert" ]] || continue
        days="$(cert_days_left "$cert" "$now_epoch" || echo "?")"
        if [[ "$days" =~ ^-?[0-9]+$ ]] && (( days <= warn_days )); then
          le_due+=("$name")
        fi
      done
    fi
  fi

  if [[ "$renew_mk" -eq 1 ]]; then
    read_lines_to_array mk_due_entries < <(collect_due_mkcert_entries "$warn_days" "$godaddy_domain" "$search_dirs" "${fqdn_filters[@]}")
  fi

  if [[ "${#le_due[@]}" -eq 0 && "${#mk_due_entries[@]}" -eq 0 ]]; then
    log "No certificate is within ${warn_days} day(s) of expiration for selected providers."
    return 0
  fi

  if [[ "${#le_due[@]}" -gt 0 ]]; then
    log "LetsEncrypt certificates due (<= ${warn_days} days): ${le_due[*]}"
  fi
  if [[ "${#mk_due_entries[@]}" -gt 0 ]]; then
    log "mkcert certificates due (<= ${warn_days} days): ${#mk_due_entries[@]}"
  fi

  if [[ "$dry_run" -eq 1 ]]; then
    if [[ "${#le_due[@]}" -gt 0 && "$renew_le" -eq 1 ]]; then
      if [[ "${#fqdn_filters[@]}" -gt 0 ]]; then
        for name in "${le_due[@]}"; do
          log "Dry-run renewal for LetsEncrypt certificate: ${name}"
          certbot renew --dry-run --cert-name "$name"
        done
      else
        certbot renew --dry-run
      fi
    fi
    if [[ "${#mk_due_entries[@]}" -gt 0 && "$renew_mk" -eq 1 ]]; then
      local mk_e mk_fqdn mk_cert mk_key
      for mk_e in "${mk_due_entries[@]}"; do
        [[ -n "$mk_e" ]] || continue
        IFS=$'\t' read -r mk_fqdn mk_cert mk_key <<< "$mk_e"
        log "Dry-run mkcert regenerate: ${mk_fqdn} (${mk_cert})"
      done
    fi
    log "Dry-run completed."
    return 0
  fi

  if [[ "${#le_due[@]}" -gt 0 && "$renew_le" -eq 1 ]]; then
    if [[ "$force_renewal" -eq 1 ]]; then
      for name in "${le_due[@]}"; do
        log "Force renewing LetsEncrypt certificate: ${name}"
        certbot certonly --cert-name "$name" --force-renewal --non-interactive
      done
    else
      if [[ "${#fqdn_filters[@]}" -gt 0 ]]; then
        for name in "${le_due[@]}"; do
          log "Renewing LetsEncrypt certificate: ${name}"
          certbot renew --cert-name "$name"
        done
      else
        certbot renew
      fi
    fi
  fi

  if [[ "${#mk_due_entries[@]}" -gt 0 && "$renew_mk" -eq 1 ]]; then
    local mk_e mk_fqdn mk_cert mk_key
    for mk_e in "${mk_due_entries[@]}"; do
      [[ -n "$mk_e" ]] || continue
      IFS=$'\t' read -r mk_fqdn mk_cert mk_key <<< "$mk_e"
      [[ -n "$mk_fqdn" ]] || continue
      log "Regenerating mkcert certificate: ${mk_fqdn}"
      issue_mkcert_cert "$mk_fqdn" "$mk_cert" "$mk_key"
    done
  fi

  if [[ "$no_reload" -eq 0 ]]; then
    reload_services_after_renew
  fi

  log "Renewal process completed."
  if [[ "${#fqdn_filters[@]}" -eq 1 ]]; then
    status_ssl --warn-days "$warn_days" --fqdn "${fqdn_filters[0]}"
  else
    status_ssl --warn-days "$warn_days"
  fi
}

list_resources() {
  local apache_dirs=()
  read_lines_to_array apache_dirs < <(detect_apache_conf_dirs)
  local apache_dir
  apache_dir="$(detect_apache_conf_dir)"

  echo "== Apache vhosts =="
  if [[ "${#apache_dirs[@]}" -gt 0 ]]; then
    local d f printed=0
    for d in "${apache_dirs[@]-}"; do
      [[ -d "$d" ]] || continue
      for f in "$d"/*; do
        is_vhost_candidate_file "$f" || continue
        if awk '
          BEGIN{IGNORECASE=1; ok=0}
          /^[[:space:]]*#/ {next}
          /<VirtualHost[[:space:]>]/ {ok=1}
          tolower($1)=="servername" {ok=1}
          END{exit ok?0:1}
        ' "$f" 2>/dev/null; then
          echo "$f"
          printed=1
        fi
      done
    done
    if [[ "$printed" -eq 0 ]]; then
      echo "(none)"
    fi
  else
    echo "(apache conf dir not found: $apache_dir)"
  fi

  echo
  echo "== Nginx vhosts =="
  if [[ -d "$DEFAULT_NGINX_CONF_DIR" ]]; then
    ls -1 "$DEFAULT_NGINX_CONF_DIR"/*.conf 2>/dev/null || echo "(none)"
  else
    echo "(nginx conf dir not found: $DEFAULT_NGINX_CONF_DIR)"
  fi

  echo
  echo "== LetsEncrypt certificates =="
  if [[ -d "$DEFAULT_LE_LIVE_DIR" ]]; then
    local found=0
    local dir
    for dir in "$DEFAULT_LE_LIVE_DIR"/*; do
      [[ -d "$dir" ]] || continue
      local name
      name="$(basename "$dir")"
      [[ "$name" == "README" ]] && continue
      echo "$name"
      found=1
    done
    if [[ "$found" -eq 0 ]]; then
      echo "(none)"
    fi
  else
    echo "(none)"
  fi

  echo
  echo "== Managed mkcert certificates =="
  if [[ -d "$DEFAULT_MKCERT_DIR" ]]; then
    local found_mk=0
    local mk_dir
    for mk_dir in "$DEFAULT_MKCERT_DIR"/*; do
      [[ -d "$mk_dir" ]] || continue
      if [[ -f "$mk_dir/fullchain.pem" && -f "$mk_dir/privkey.pem" ]]; then
        echo "$(basename "$mk_dir")"
        found_mk=1
      fi
    done
    if [[ "$found_mk" -eq 0 ]]; then
      echo "(none)"
    fi
  else
    echo "(none)"
  fi
}

delete_site() {
  local fqdn=""
  local server="auto"
  local vhost_file=""
  local delete_le=0
  local delete_mk=0
  local force=0
  local non_interactive=0

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --fqdn) fqdn="${2:-}"; shift 2 ;;
      --server) server="${2:-}"; shift 2 ;;
      --vhost-file) vhost_file="${2:-}"; shift 2 ;;
      --delete-letsencrypt) delete_le=1; shift ;;
      --delete-mkcert) delete_mk=1; shift ;;
      --force) force=1; shift ;;
      --non-interactive) non_interactive=1; shift ;;
      --help|-h)
        print_usage
        exit 0
        ;;
      *) die "Unknown delete option: $1" ;;
    esac
  done

  require_root

  if [[ -z "$fqdn" && "$non_interactive" -eq 0 ]]; then
    ask_text fqdn "FQDN to delete" ""
  fi

  [[ -n "$fqdn" ]] || die "--fqdn is required"
  fqdn="$(normalize_lower "$fqdn")"

  [[ "$server" == "auto" || "$server" == "apache" || "$server" == "nginx" ]] || die "--server must be auto|apache|nginx"

  local apache_changed=0
  local nginx_changed=0
  local apache_file nginx_file
  local apache_targets=()
  local nginx_targets=()
  local f
  local dup s

  apache_file="$(get_vhost_file apache "$fqdn")"
  nginx_file="$(get_vhost_file nginx "$fqdn")"

  add_unique_path() {
    local __arr_name="$1"
    local __value="$2"
    local _e
    local _dup=0
    [[ -n "$__value" ]] || return 0
    eval "for _e in \"\${${__arr_name}[@]-}\"; do
      if [[ \"\$_e\" == \"$__value\" ]]; then _dup=1; break; fi
    done"
    if [[ "$_dup" -eq 0 ]]; then
      eval "${__arr_name}+=(\"$__value\")"
    fi
  }

  if [[ "$server" == "auto" || "$server" == "apache" ]]; then
    if [[ -n "$vhost_file" ]]; then
      add_unique_path apache_targets "$vhost_file"
    fi
    add_unique_path apache_targets "$apache_file"
    while IFS= read -r f; do
      add_unique_path apache_targets "$f"
    done < <(find_apache_vhost_files_by_fqdn "$fqdn")
  fi

  if [[ "$server" == "auto" || "$server" == "nginx" ]]; then
    if [[ -n "$vhost_file" ]]; then
      add_unique_path nginx_targets "$vhost_file"
    fi
    add_unique_path nginx_targets "$nginx_file"
    while IFS= read -r f; do
      add_unique_path nginx_targets "$f"
    done < <(find_nginx_vhost_files_by_fqdn "$fqdn")
  fi

  if [[ "$force" -ne 1 && "$non_interactive" -eq 0 ]]; then
    if ! ask_yes_no "Delete vhost for ${fqdn}?" "no"; then
      die "Cancelled by user."
    fi
  fi

  if [[ "$server" == "auto" || "$server" == "apache" ]]; then
    for f in "${apache_targets[@]-}"; do
      [[ -f "$f" ]] || continue
      disable_apache_site_if_needed "$f"
      rm -f "$f"
      apache_changed=1
      log "Deleted Apache vhost: $f"
    done
  fi

  if [[ "$server" == "auto" || "$server" == "nginx" ]]; then
    for f in "${nginx_targets[@]-}"; do
      [[ -f "$f" ]] || continue
      rm -f "$f"
      nginx_changed=1
      log "Deleted Nginx vhost: $f"
    done
  fi

  if [[ "$delete_le" -eq 1 ]]; then
    if [[ -d "${DEFAULT_LE_LIVE_DIR}/${fqdn}" ]]; then
      if command -v certbot >/dev/null 2>&1; then
        certbot delete --cert-name "$fqdn" --non-interactive || warn "Certbot delete failed for ${fqdn}."
      else
        warn "certbot not installed; cannot delete LetsEncrypt cert automatically."
      fi
    fi
  fi

  if [[ "$delete_mk" -eq 1 ]]; then
    local mk_dir="${DEFAULT_MKCERT_DIR}/${fqdn}"
    if [[ -d "$mk_dir" ]]; then
      rm -f "$mk_dir/fullchain.pem" "$mk_dir/privkey.pem"
      rmdir "$mk_dir" 2>/dev/null || true
      log "Deleted managed mkcert assets for ${fqdn}: ${mk_dir}"
    else
      warn "No managed mkcert directory found for ${fqdn}: ${mk_dir}"
    fi
  fi

  local before_hosts_count after_hosts_count
  before_hosts_count="$(hosts_managed_count_for_fqdn "$fqdn" "$DEFAULT_HOSTS_FILE")"
  remove_managed_hosts_entry "$fqdn" "$DEFAULT_HOSTS_FILE"
  after_hosts_count="$(hosts_managed_count_for_fqdn "$fqdn" "$DEFAULT_HOSTS_FILE")"
  if [[ "$before_hosts_count" != "$after_hosts_count" ]]; then
    log "Hosts cleanup: removed ${fqdn} from ${DEFAULT_HOSTS_FILE}"
  fi

  if [[ "$apache_changed" -eq 1 ]]; then
    try_reload_apache_nonfatal || true
  fi
  if [[ "$nginx_changed" -eq 1 ]]; then
    try_reload_nginx_nonfatal || true
  fi

  if [[ "$apache_changed" -eq 0 && "$nginx_changed" -eq 0 ]]; then
    warn "No vhost file found for ${fqdn}"
  fi
}

pause_enter() {
  echo
  read -r -p "Press ENTER to continue..." _ || true
}

menu_run_action() {
  local rc=0
  "$@" || rc=$?

  if [[ "$rc" -eq 130 ]]; then
    return 130
  fi
  return "$rc"
}

collect_vhost_browser_rows() {
  local warn_days="$1"
  local godaddy_domain="$2"
  local search_dirs="$3"

  local entries=()
  read_lines_to_array entries < <(collect_all_inventory_entries "$godaddy_domain" "$search_dirs")
  local now_epoch
  now_epoch="$(date +%s)"

  local e fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file
  local state days expires_at renew label
  for e in "${entries[@]-}"; do
    [[ -n "$e" ]] || continue
    IFS=$'\x1f' read -r fqdn server mode has_tls provider cert_file key_file docroot upstream vhost_file <<< "$e"
    [[ -n "$fqdn" ]] || continue

    compute_ssl_state "$cert_file" "$provider" "$warn_days" "$now_epoch" state days expires_at renew
    label="${fqdn} $(ssl_state_colored_paren "$state")"
    printf "%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\n" \
      "$label" "$fqdn" "$server" "$mode" "$provider" "$state" "$days" "$expires_at" "$cert_file" "$key_file" "$docroot" "$upstream" "$vhost_file"
  done | sort -t $'\x1f' -k2,2 -k3,3
}

browser_is_selected_key() {
  local key="$1"
  local k
  for k in "${BROWSER_SELECTED_KEYS[@]-}"; do
    if [[ "$k" == "$key" ]]; then
      return 0
    fi
  done
  return 1
}

browser_toggle_selected_key() {
  local key="$1"
  local next=()
  local found=0
  local k
  for k in "${BROWSER_SELECTED_KEYS[@]-}"; do
    if [[ "$k" == "$key" ]]; then
      found=1
    else
      next+=("$k")
    fi
  done
  if [[ "$found" -eq 0 ]]; then
    next+=("$key")
  fi
  BROWSER_SELECTED_KEYS=("${next[@]}")
}

browser_count_selected_keys() {
  local count=0
  local k
  for k in "${BROWSER_SELECTED_KEYS[@]-}"; do
    [[ -n "$k" ]] || continue
    count=$((count + 1))
  done
  echo "$count"
}

browser_collect_selected_rows() {
  local __out_var="$1"
  shift
  local all_rows=("$@")
  local out=()
  local row sel_label sel_fqdn sel_server sel_mode sel_provider sel_state sel_days sel_expires sel_cert sel_key sel_docroot sel_upstream sel_vhost

  for row in "${all_rows[@]-}"; do
    [[ -n "$row" ]] || continue
    IFS=$'\x1f' read -r sel_label sel_fqdn sel_server sel_mode sel_provider sel_state sel_days sel_expires sel_cert sel_key sel_docroot sel_upstream sel_vhost <<< "$row"
    if browser_is_selected_key "${sel_fqdn}|${sel_server}"; then
      out+=("$row")
    fi
  done

  eval "$__out_var=()"
  local r
  for r in "${out[@]-}"; do
    eval "$__out_var+=(\"\$r\")"
  done
}

confirm_yes_no_prompt() {
  local ans=""
  while true; do
    printf "%bEstas seguro? (s/n): %b" "$ORANGE" "$RESET"
    IFS= read -rsn1 ans || true
    if [[ "$ans" =~ [[:print:]] ]]; then
      printf "%s" "$ans"
    fi
    echo

    if [[ "$UI_CANCEL_REQUESTED" -eq 1 ]]; then
      UI_CANCEL_REQUESTED=0
      return 130
    fi
    if [[ "$ans" == $'\x18' ]]; then
      exit 0
    fi
    if [[ "$ans" == $'\x03' ]]; then
      return 130
    fi

    case "$ans" in
      s|S) return 0 ;;
      n|N|"") return 1 ;;
      $'\x1b') return 1 ;;
      *) ;;
    esac
  done
}

confirm_delete_prompt() {
  confirm_yes_no_prompt
}

confirm_update_ssl_prompt() {
  confirm_yes_no_prompt
}

show_vhost_ssl_detail() {
  local row="$1"
  local label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file
  IFS=$'\x1f' read -r label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file <<< "$row"
  local ssl_subject="-"
  local ssl_issuer="-"
  local ssl_not_before="-"
  local ssl_not_after="-"

  ui_page_begin_with_header
  echo "------------------------------------------"
  printf "🏠 > Detalle\n"
  echo "------------------------------------------"
  echo
  ui_key_help_detail
  echo "Domain name      : ${fqdn:-"-"}"
  echo "Estado SSL       : $(ssl_state_colored_paren "${state:-"-"}")"
  echo "Dias restantes   : ${days:-"-"}"
  echo "Expira           : ${expires_at:-"-"}"
  echo "Servidor         : ${server:-"-"}"
  echo "Modo             : ${mode:-"-"}"
  echo "Proveedor SSL    : ${provider:-"-"}"
  echo "Vhost file       : ${vhost_file:--}"
  echo "DocumentRoot     : ${docroot:--}"
  echo "ReverseProxy     : ${upstream:--}"
  echo "Cert file        : ${cert_file:--}"
  echo "Key file         : ${key_file:--}"
  echo

  if [[ -n "$cert_file" && -f "$cert_file" ]]; then
    ssl_subject="$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | sed 's/^subject=//')"
    ssl_issuer="$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | sed 's/^issuer=//')"
    ssl_not_before="$(openssl x509 -in "$cert_file" -noout -startdate 2>/dev/null | sed 's/^notBefore=//')"
    ssl_not_after="$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | sed 's/^notAfter=//')"
    [[ -n "$ssl_subject" ]] || ssl_subject="-"
    [[ -n "$ssl_issuer" ]] || ssl_issuer="-"
    [[ -n "$ssl_not_before" ]] || ssl_not_before="-"
    [[ -n "$ssl_not_after" ]] || ssl_not_after="-"

    echo "SSL subject      : ${ssl_subject}"
    echo "SSL issuer       : ${ssl_issuer}"
    echo "Valido desde     : ${ssl_not_before}"
    echo "Valido hasta     : ${ssl_not_after}"
    echo
  fi
  echo "Presiona ENTER o ESC para regresar..."
  ui_drain_input
  while true; do
    local key
    key="$(ui_read_key || true)"
    case "$key" in
      enter|esc|ctrl_c)
        return 0
        ;;
      ctrl_x)
        exit 0
        ;;
      *)
        ;;
    esac
  done
}

guess_provider_quick() {
  local fqdn="$1"
  local cert_file="$2"
  local godaddy_domain="$3"

  if [[ -z "$cert_file" || ! -f "$cert_file" ]]; then
    echo "none"
    return 0
  fi

  if [[ "$cert_file" == /etc/letsencrypt/live/* ]]; then
    echo "letsencrypt"
    return 0
  fi

  if [[ "$cert_file" == "${DEFAULT_MKCERT_DIR}/"* ]] || cert_issuer_is_mkcert "$cert_file"; then
    echo "mkcert"
    return 0
  fi

  if [[ "$fqdn" == "$godaddy_domain" || "$fqdn" == *".${godaddy_domain}" ]]; then
    echo "godaddy"
    return 0
  fi

  echo "custom"
}

find_created_vhost_fast() {
  local fqdn="$1"
  local godaddy_domain="$2"
  local __row_var="$3"

  local file server mode provider cert_file key_file docroot upstream

  file="$(get_vhost_file apache "$fqdn")"
  if [[ -f "$file" ]]; then
    server="apache"
    cert_file="$(awk 'tolower($1)=="sslcertificatefile"{print $2; exit}' "$file" 2>/dev/null || true)"
    key_file="$(awk 'tolower($1)=="sslcertificatekeyfile"{print $2; exit}' "$file" 2>/dev/null || true)"
    docroot="$(awk 'tolower($1)=="documentroot"{print $2; exit}' "$file" 2>/dev/null || true)"
    cert_file="$(clean_conf_value "$cert_file")"
    key_file="$(clean_conf_value "$key_file")"
    docroot="$(clean_conf_value "$docroot")"
    mode="static"
    upstream=""
    if grep -Eiq '^[[:space:]]*ProxyPass[[:space:]]+' "$file"; then
      mode="proxy"
      upstream="$(awk 'tolower($1)=="proxypass"{print $3; exit}' "$file" 2>/dev/null || true)"
      upstream="$(clean_conf_value "$upstream")"
    fi
    provider="$(guess_provider_quick "$fqdn" "$cert_file" "$godaddy_domain")"
    printf -v "$__row_var" "%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\n" \
      "$fqdn" "$server" "$mode" "$provider" "$cert_file" "$key_file" "$docroot" "$upstream"
    return 0
  fi

  file="$(get_vhost_file nginx "$fqdn")"
  if [[ -f "$file" ]]; then
    server="nginx"
    cert_file="$(awk '$1=="ssl_certificate"{print $2; exit}' "$file" 2>/dev/null || true)"
    key_file="$(awk '$1=="ssl_certificate_key"{print $2; exit}' "$file" 2>/dev/null || true)"
    docroot="$(awk '$1=="root"{print $2; exit}' "$file" 2>/dev/null || true)"
    cert_file="$(clean_conf_value "$cert_file")"
    key_file="$(clean_conf_value "$key_file")"
    docroot="$(clean_conf_value "$docroot")"
    mode="static"
    upstream=""
    if grep -Eiq '^[[:space:]]*proxy_pass[[:space:]]+' "$file"; then
      mode="proxy"
      upstream="$(awk '$1=="proxy_pass"{print $2; exit}' "$file" 2>/dev/null || true)"
      upstream="$(clean_conf_value "$upstream")"
    fi
    provider="$(guess_provider_quick "$fqdn" "$cert_file" "$godaddy_domain")"
    printf -v "$__row_var" "%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\n" \
      "$fqdn" "$server" "$mode" "$provider" "$cert_file" "$key_file" "$docroot" "$upstream"
    return 0
  fi

  return 1
}

validate_created_site() {
  local fqdn="$1"
  local godaddy_domain="$2"
  local search_dirs="$3"
  local warn_days=30
  local now_epoch
  now_epoch="$(date +%s)"
  local fast_row=""
  local server mode provider cert_file key_file docroot upstream
  local state days expires_at renew

  if find_created_vhost_fast "$fqdn" "$godaddy_domain" fast_row; then
    IFS=$'\x1f' read -r _ server mode provider cert_file key_file docroot upstream <<< "$fast_row"
    compute_ssl_state "$cert_file" "$provider" "$warn_days" "$now_epoch" state days expires_at renew
  else
    # Fallback to deep inventory scan only if fast path failed.
    local rows=()
    read_lines_to_array rows < <(collect_vhost_browser_rows "$warn_days" "$godaddy_domain" "$search_dirs")
    local row label rfqdn
    local found=0
    for row in "${rows[@]-}"; do
      [[ -n "$row" ]] || continue
      IFS=$'\x1f' read -r label rfqdn server mode provider state days expires_at cert_file key_file docroot upstream _ <<< "$row"
      if [[ "$rfqdn" == "$fqdn" ]]; then
        found=1
        break
      fi
    done
    if [[ "$found" -eq 0 ]]; then
      warn "No se pudo confirmar el vhost para ${fqdn} en inventario."
      return 1
    fi
  fi

  if [[ "$state" == "OK" || "$state" == "EXPIRING" ]]; then
    log "Validacion OK: ${fqdn} con SSL $(ssl_state_colored_paren "$state") (expira: ${expires_at})."
  else
    warn "Validacion parcial: ${fqdn} con estado SSL $(ssl_state_colored_paren "$state")."
  fi

  local resolved_ip
  resolved_ip="$(resolve_fqdn_ipv4 "$fqdn")"
  if [[ -n "$resolved_ip" ]]; then
    if command -v curl >/dev/null 2>&1; then
      if curl -4 -kfsS --connect-timeout 2 --max-time 4 "https://${fqdn}" >/dev/null 2>&1; then
        log "HTTPS responde correctamente para ${fqdn} (IP: ${resolved_ip})."
      else
        warn "No se pudo validar respuesta HTTPS por curl para ${fqdn} (IP: ${resolved_ip})."
      fi
    fi
  else
    warn "No se valido respuesta HTTPS por DNS/hosts para ${fqdn} (no resuelto)."
  fi
}

create_and_validate_site() {
  local fqdn="$1"
  local godaddy_domain="$2"
  local search_dirs="$3"
  shift 3

  create_site "$@"
  validate_created_site "$fqdn" "$godaddy_domain" "$search_dirs" || true
}

menu_create_screen_flow() {
  menu_create_transparent_flow
}

menu_create_transparent_flow() {
  local cert_profile="custom"
  local fqdn=""
  local server="apache"
  local site_type="documentroot"
  local mode="static"
  local document_root=""
  local upstream="http://127.0.0.1:3000"
  local email=""
  local cert_provider="custom"
  local cert_file=""
  local key_file=""
  local godaddy_domain="$DEFAULT_GODADDY_DOMAIN"
  local search_dirs="$DEFAULT_GODADDY_SEARCH_DIRS"
  local i
  local fqdn_input=""
  local profile_opts=(custom letsencrypt mkcert)
  local profile_idx=0
  local server_opts=(apache nginx)
  local server_idx=0
  local site_opts=(documentroot reverseproxy)
  local site_idx=0

  if [[ "${EUID}" -ne 0 && "${SSL_ADMIN_SKIP_SUDO_CHECK:-0}" != "1" ]]; then
    render_create_wizard_screen "$cert_profile" "$fqdn" "$site_type" "$mode" "$document_root" "$upstream" "$email"
    printf "[!] Se requiere sudo para crear vhost/SSL. Ejecuta: sudo %s menu\n" "$0"
    echo
    echo "Presiona una tecla para regresar..."
    ui_read_key >/dev/null || true
    return 130
  fi

  for ((i=0; i<${#profile_opts[@]}; i++)); do
    if [[ "${profile_opts[$i]}" == "$cert_profile" ]]; then
      profile_idx="$i"
      break
    fi
  done

  render_create_wizard_screen "$cert_profile" "$fqdn" "$site_type" "$mode" "$document_root" "$upstream" "$email"

  ui_select_horizontal_inline cert_profile "Tipo de dominio" "${profile_opts[$profile_idx]}" "${profile_opts[@]}" || return $?

  while true; do
    ask_text fqdn_input "Domain name (FQDN o subdominio)" "$fqdn" || return $?
    fqdn_input="$(normalize_lower "$(trim_spaces "$fqdn_input")")"
    fqdn_input="${fqdn_input%.}"

    if [[ -z "$fqdn_input" ]]; then
      printf "[!] Domain name no puede estar vacio.\n"
      continue
    fi

    fqdn="$fqdn_input"
    if [[ "$cert_profile" == "custom" ]]; then
      cert_provider="custom"
    elif [[ "$cert_profile" == "letsencrypt" ]]; then
      cert_provider="letsencrypt"
    else
      cert_provider="mkcert"
      if [[ "$fqdn" != *.* ]]; then
        fqdn="${fqdn}.local"
      fi
    fi

    if ! is_valid_fqdn "$fqdn"; then
      printf "[!] Formato de dominio invalido: %s\n" "$fqdn"
      continue
    fi

    break
  done

  for ((i=0; i<${#site_opts[@]}; i++)); do
    if [[ "${site_opts[$i]}" == "$site_type" ]]; then
      site_idx="$i"
      break
    fi
  done

  ui_select_horizontal_inline site_type "Tipo de sitio" "${site_opts[$site_idx]}" "${site_opts[@]}" || return $?
  ui_select_horizontal_inline server "Web server" "${server_opts[$server_idx]}" "${server_opts[@]}" || return $?

  if [[ "$site_type" == "reverseproxy" ]]; then
    mode="proxy"
  else
    mode="static"
  fi
  if [[ "$mode" == "static" ]]; then
    while true; do
      ask_text document_root "DocumentRoot" "${DEFAULT_WEBROOT_BASE}/${fqdn}/public" || return $?
      document_root="$(trim_spaces "$document_root")"
      if ! is_valid_document_root_path "$document_root"; then
        printf "[!] DocumentRoot invalido. Usa ruta absoluta sin espacios (ej: /var/www/%s/public).\n" "$fqdn"
        continue
      fi
      break
    done
  else
    while true; do
      ask_text upstream "ReverseProxy URL" "$upstream" || return $?
      upstream="$(trim_spaces "$upstream")"
      if ! is_valid_http_url "$upstream"; then
        printf "[!] ReverseProxy URL invalida. Usa http(s)://host[:puerto][/ruta]\n"
        continue
      fi
      break
    done
  fi

  if [[ "$cert_provider" == "letsencrypt" ]]; then
    while true; do
      ask_text email "Correo para LetsEncrypt" "$email" || return $?
      email="$(normalize_lower "$(trim_spaces "$email")")"
      if [[ -z "$email" ]]; then
        printf "[!] Correo para LetsEncrypt es obligatorio.\n"
        continue
      fi
      if ! is_valid_email_basic "$email"; then
        printf "[!] Formato de correo invalido.\n"
        continue
      fi
      break
    done
  fi

  if [[ "$cert_provider" == "custom" ]]; then
    prompt_for_existing_cert_key "$fqdn" "$cert_file" "$key_file" cert_file key_file || return $?
  fi

  local args=(
    --fqdn "$fqdn"
    --server "$server"
    --mode "$mode"
    --cert-provider "$cert_provider"
    --godaddy-domain "$godaddy_domain"
    --search-dirs "$search_dirs"
    --force
    --non-interactive
  )

  if [[ "$mode" == "static" ]]; then
    args+=(--document-root "$document_root")
  else
    args+=(--upstream "$upstream")
  fi
  if [[ -n "$email" ]]; then
    args+=(--email "$email")
  fi
  if [[ "$cert_provider" == "custom" ]]; then
    args+=(--cert "$cert_file" --key "$key_file")
  fi

  if [[ "${SSL_ADMIN_SKIP_SUDO_CHECK:-0}" == "1" && "${EUID}" -ne 0 ]]; then
    ui_run_with_spinner "Terminando configuracion ..." bash -c 'sleep 0.8'
    warn "Modo prueba activo (SSL_ADMIN_SKIP_SUDO_CHECK=1): no se aplicaron cambios reales."
    log "Simulacion create --fqdn ${fqdn} --server ${server} --mode ${mode} --cert-provider ${cert_provider}"
    return 0
  fi

  ui_run_with_spinner "Terminando configuracion ..." \
    create_and_validate_site "$fqdn" "$godaddy_domain" "$search_dirs" "${args[@]}"
}

delete_vhost_from_row() {
  local row="$1"
  local label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file
  IFS=$'\x1f' read -r label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file <<< "$row"

  local args=(--fqdn "$fqdn" --server "$server" --force --non-interactive)
  if [[ -n "$vhost_file" && "$vhost_file" != "-" ]]; then
    args+=(--vhost-file "$vhost_file")
  fi
  if [[ "$provider" == "letsencrypt" ]]; then
    args+=(--delete-letsencrypt)
  fi
  if [[ "$provider" == "mkcert" ]]; then
    args+=(--delete-mkcert)
  fi

  delete_site "${args[@]}"
  if [[ "$provider" == "godaddy" || "$provider" == "custom" ]]; then
    warn "SSL compartido (${provider}) no se elimina automaticamente del filesystem."
  fi
}

prompt_for_existing_cert_key() {
  local fqdn="$1"
  local default_cert="${2:-}"
  local default_key="${3:-}"
  local __cert_var="$4"
  local __key_var="$5"

  local cert_input="$default_cert"
  local key_input="$default_key"
  local resolved_cert=""
  local resolved_key=""

  while true; do
    ask_text cert_input "Ruta cert (archivo o carpeta)" "$cert_input" || return $?
    cert_input="$(trim_spaces "$cert_input")"
    ask_text key_input "Ruta key (archivo o carpeta)" "$key_input" || return $?
    key_input="$(trim_spaces "$key_input")"

    if [[ -z "$cert_input" || -z "$key_input" ]]; then
      printf "[!] Debes ingresar ruta de certificado y llave privada.\n"
      continue
    fi

    if ! resolve_existing_cert_key_paths "$cert_input" "$key_input" resolved_cert resolved_key; then
      printf "[!] No se pudo resolver un par cert/key valido con esas rutas.\n"
      printf "    Puedes usar archivo directo o carpeta (fullchain/cert + privkey/key).\n"
      continue
    fi

    if ! cert_covers_fqdn "$resolved_cert" "$fqdn"; then
      printf "[!] El certificado seleccionado no cubre %s.\n" "$fqdn"
      continue
    fi

    printf -v "$__cert_var" "%s" "$resolved_cert"
    printf -v "$__key_var" "%s" "$resolved_key"
    return 0
  done
}

update_vhost_tls_paths() {
  local server="$1"
  local vhost_file="$2"
  local cert_file="$3"
  local key_file="$4"
  local tmp_file

  [[ -f "$vhost_file" ]] || die "Vhost file not found: ${vhost_file}"

  tmp_file="$(mktemp -t ssl-admin-vhost.XXXXXX)"
  if [[ "$server" == "apache" ]]; then
    awk -v cert="$cert_file" -v key="$key_file" '
      BEGIN { has_cert=0; has_key=0 }
      {
        if (tolower($1)=="sslcertificatefile") {
          indent=match($0, /[^[:space:]]/)
          if (indent < 1) indent=1
          pre=substr($0, 1, indent-1)
          print pre "SSLCertificateFile " cert
          has_cert=1
          next
        }
        if (tolower($1)=="sslcertificatekeyfile") {
          indent=match($0, /[^[:space:]]/)
          if (indent < 1) indent=1
          pre=substr($0, 1, indent-1)
          print pre "SSLCertificateKeyFile " key
          has_key=1
          next
        }
        print
      }
      END { if (has_cert==0 || has_key==0) exit 3 }
    ' "$vhost_file" > "$tmp_file" || {
      rm -f "$tmp_file"
      die "No se pudieron actualizar directivas SSL en ${vhost_file} (apache)."
    }
  elif [[ "$server" == "nginx" ]]; then
    awk -v cert="$cert_file" -v key="$key_file" '
      BEGIN { has_cert=0; has_key=0 }
      {
        if ($1=="ssl_certificate") {
          indent=match($0, /[^[:space:]]/)
          if (indent < 1) indent=1
          pre=substr($0, 1, indent-1)
          print pre "ssl_certificate " cert ";"
          has_cert=1
          next
        }
        if ($1=="ssl_certificate_key") {
          indent=match($0, /[^[:space:]]/)
          if (indent < 1) indent=1
          pre=substr($0, 1, indent-1)
          print pre "ssl_certificate_key " key ";"
          has_key=1
          next
        }
        print
      }
      END { if (has_cert==0 || has_key==0) exit 3 }
    ' "$vhost_file" > "$tmp_file" || {
      rm -f "$tmp_file"
      die "No se pudieron actualizar directivas SSL en ${vhost_file} (nginx)."
    }
  else
    rm -f "$tmp_file"
    die "Unsupported server for TLS update: ${server}"
  fi

  cat "$tmp_file" > "$vhost_file"
  rm -f "$tmp_file"
}

update_ssl_from_row() {
  local row="$1"
  local label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file
  IFS=$'\x1f' read -r label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file <<< "$row"
  local new_cert=""
  local new_key=""

  case "$provider" in
    letsencrypt)
      if [[ "${SSL_ADMIN_SKIP_SUDO_CHECK:-0}" == "1" && "${EUID}" -ne 0 ]]; then
        log "Simulacion update SSL LetsEncrypt para ${fqdn}"
        return 0
      fi
      renew_ssl --warn-days 36500 --fqdn "$fqdn" --force-renewal --no-reload
      ;;
    mkcert)
      if [[ -z "$cert_file" || -z "$key_file" ]]; then
        warn "No se encontraron rutas cert/key para mkcert en ${fqdn}. Se omite."
        return 0
      fi
      if [[ "${SSL_ADMIN_SKIP_SUDO_CHECK:-0}" == "1" && "${EUID}" -ne 0 ]]; then
        log "Simulacion update SSL mkcert para ${fqdn}"
        return 0
      fi
      issue_mkcert_cert "$fqdn" "$cert_file" "$key_file"
      ;;
    godaddy|custom)
      if [[ -z "$vhost_file" || "$vhost_file" == "-" ]]; then
        warn "No se encontro archivo de vhost para ${fqdn}. Se omite."
        return 0
      fi
      prompt_for_existing_cert_key "$fqdn" "$cert_file" "$key_file" new_cert new_key || return $?
      if [[ "${SSL_ADMIN_SKIP_SUDO_CHECK:-0}" == "1" && "${EUID}" -ne 0 ]]; then
        log "Simulacion update SSL ${provider} para ${fqdn}"
        log "Nuevo cert: ${new_cert}"
        log "Nueva key: ${new_key}"
        return 0
      fi
      update_vhost_tls_paths "$server" "$vhost_file" "$new_cert" "$new_key"
      log "SSL actualizado para ${fqdn} (${provider})"
      ;;
    none)
      warn "Dominio ${fqdn} sin SSL configurado. Usa N para crear con certificado."
      ;;
    *)
      warn "Proveedor SSL desconocido (${provider}) para ${fqdn}. Se omite."
      ;;
  esac
}

menu_update_ssl_rows() {
  local rows=("$@")
  local row
  local changed=0

  for row in "${rows[@]-}"; do
    [[ -n "$row" ]] || continue
    update_ssl_from_row "$row" || return $?
    changed=1
  done

  if [[ "$changed" -eq 1 ]]; then
    if [[ "${SSL_ADMIN_SKIP_SUDO_CHECK:-0}" == "1" && "${EUID}" -ne 0 ]]; then
      log "Simulacion update SSL completada."
    else
      reload_services_after_renew
      log "Actualizacion SSL completada."
    fi
  fi
}

run_menu() {
  # Interactive mode should never abort the whole script on a single non-zero read/action.
  set +e
  local warn_days=30
  local godaddy_domain="$DEFAULT_GODADDY_DOMAIN"
  local search_dirs="$DEFAULT_GODADDY_SEARCH_DIRS"
  local cursor=0
  local need_full_redraw=1
  local reload_rows=1
  local list_dirty=1
  local rows_rendered=0
  local row_count=0
  local total=0
  local rows=()
  local view_rows=()
  local options=()
  BROWSER_SELECTED_KEYS=()
  ui_enter_alt_screen

  while true; do
    if [[ "$reload_rows" -eq 1 ]]; then
      rows=()
      view_rows=()
      options=()
      read_lines_to_array rows < <(collect_vhost_browser_rows "$warn_days" "$godaddy_domain" "$search_dirs")

      local row label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file
      for row in "${rows[@]-}"; do
        [[ -n "$row" ]] || continue
        IFS=$'\x1f' read -r label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file <<< "$row"
        view_rows+=("$row")
        options+=("$label")
      done
      row_count="${#options[@]}"
      total="$row_count"
      (( total >= 0 )) || total=0
      if (( cursor >= total )); then
        if (( total > 0 )); then
          cursor=$((total - 1))
        else
          cursor=0
        fi
      fi
      if (( cursor < 0 )); then
        cursor=0
      fi
      reload_rows=0
      list_dirty=1
    fi

    if [[ "$need_full_redraw" -eq 1 ]]; then
      ui_page_begin_with_header
      echo "------------------------------------------"
      printf "🏠 > Inicio\n"
      echo "------------------------------------------"
      echo 
      ui_key_help_main
      rows_rendered=0
      list_dirty=1
      local i mark current_key
      need_full_redraw=0
    fi

    if [[ "$list_dirty" -eq 1 ]]; then
      ui_clear_prev_lines "$rows_rendered"
      local i mark current_key
      for ((i=0; i<total; i++)); do
        mark=" "
        if (( i < row_count )); then
          IFS=$'\x1f' read -r label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file <<< "${view_rows[$i]}"
          current_key="${fqdn}|${server}"
          if browser_is_selected_key "$current_key"; then
            mark="x"
          fi
        fi
        if (( i == cursor )); then
          printf "> [%s] %s\n" "$mark" "${options[$i]}"
        else
          printf "  [%s] %s\n" "$mark" "${options[$i]}"
        fi
      done
      if (( row_count == 0 )); then
        echo "(sin vhosts detectados)"
        echo
        echo "[ ESC/Ctrl+C: Inicio | Ctrl+X: Salir ]"
        rows_rendered=3
      else
        echo
        echo "[ ESC/Ctrl+C: Inicio | Ctrl+X: Salir ]"
        rows_rendered=$((row_count + 2))
      fi
      list_dirty=0
    fi

    local key
    key="$(ui_read_key || true)"
    case "$key" in
      up)
        if (( total > 0 )); then
          cursor=$(( (cursor - 1 + total) % total ))
          list_dirty=1
        fi
        ;;
      down)
        if (( total > 0 )); then
          cursor=$(( (cursor + 1) % total ))
          list_dirty=1
        fi
        ;;
      space)
        if (( cursor < row_count )); then
          IFS=$'\x1f' read -r label fqdn server mode provider state days expires_at cert_file key_file docroot upstream vhost_file <<< "${view_rows[$cursor]}"
          browser_toggle_selected_key "${fqdn}|${server}"
          list_dirty=1
        fi
        ;;
      enter)
        if (( cursor < row_count )); then
          show_vhost_ssl_detail "${view_rows[$cursor]}"
          need_full_redraw=1
          reload_rows=1
          list_dirty=1
        fi
        ;;
      new_domain)
        if menu_run_action menu_create_screen_flow; then
          pause_enter
          reload_rows=1
        else
          BROWSER_SELECTED_KEYS=()
        fi
        need_full_redraw=1
        reload_rows=1
        list_dirty=1
        ;;
      update_ssl)
        local selected_count_update=0
        selected_count_update="$(browser_count_selected_keys)"
        if [[ "$selected_count_update" -gt 0 ]]; then
          local update_rows=()
          local needs_manual_input=0
          local urow u_provider
          browser_collect_selected_rows update_rows "${view_rows[@]}"
          if [[ "${#update_rows[@]}" -gt 0 ]]; then
            for urow in "${update_rows[@]-}"; do
              [[ -n "$urow" ]] || continue
              IFS=$'\x1f' read -r _ _ _ _ u_provider _ _ _ _ _ _ _ _ <<< "$urow"
              if [[ "$u_provider" == "custom" || "$u_provider" == "godaddy" ]]; then
                needs_manual_input=1
                break
              fi
            done
            echo
            if confirm_update_ssl_prompt; then
              if [[ "$needs_manual_input" -eq 1 ]]; then
                printf "%bActualizando SSL ...%b\n" "$ORANGE" "$RESET"
                menu_run_action menu_update_ssl_rows "${update_rows[@]}"
              else
                ui_run_with_spinner "Actualizando SSL ..." \
                  menu_run_action menu_update_ssl_rows "${update_rows[@]}"
              fi
              BROWSER_SELECTED_KEYS=()
              reload_rows=1
              pause_enter
              need_full_redraw=1
              reload_rows=1
              list_dirty=1
            else
              BROWSER_SELECTED_KEYS=()
              need_full_redraw=1
              reload_rows=1
              list_dirty=1
            fi
          fi
        fi
        ;;
      backspace)
        local delete_rows=()
        local selected_count=0
        selected_count="$(browser_count_selected_keys)"
        if [[ "$selected_count" -gt 0 ]]; then
          browser_collect_selected_rows delete_rows "${view_rows[@]}"
        fi

        local has_delete_rows=0
        local _tmp_delete_row
        for _tmp_delete_row in "${delete_rows[@]-}"; do
          [[ -n "$_tmp_delete_row" ]] || continue
          has_delete_rows=1
          break
        done
        if [[ "$has_delete_rows" -eq 1 ]]; then
          echo
          if confirm_delete_prompt; then
            local drow
            for drow in "${delete_rows[@]}"; do
              menu_run_action delete_vhost_from_row "$drow" || true
            done
            BROWSER_SELECTED_KEYS=()
            reload_rows=1
            pause_enter
          else
            BROWSER_SELECTED_KEYS=()
          fi
          need_full_redraw=1
          reload_rows=1
          list_dirty=1
        fi
        ;;
      ctrl_c)
        UI_CANCEL_REQUESTED=0
        ui_drain_input
        BROWSER_SELECTED_KEYS=()
        need_full_redraw=1
        reload_rows=1
        list_dirty=1
        ;;
      esc)
        UI_CANCEL_REQUESTED=0
        ui_drain_input
        BROWSER_SELECTED_KEYS=()
        need_full_redraw=1
        reload_rows=1
        list_dirty=1
        ;;
      ctrl_x)
        exit 0
        ;;
      *)
        ;;
    esac
  done
}

main() {
  ensure_sudo_session "$@"

  local cmd="${1:-menu}"
  if [[ $# -gt 0 ]]; then shift; fi

  case "$cmd" in
    create)
      create_site "$@"
      ;;
    dashboard)
      dashboard_ssl "$@"
      ;;
    status)
      status_ssl "$@"
      ;;
    renew)
      renew_ssl "$@"
      ;;
    list)
      list_resources "$@"
      ;;
    doctor)
      doctor_apache_ssl "$@"
      ;;
    find-godaddy-certs)
      local domain="$DEFAULT_GODADDY_DOMAIN"
      local search_dirs="$DEFAULT_GODADDY_SEARCH_DIRS"
      while [[ $# -gt 0 ]]; do
        case "$1" in
          --domain) domain="${2:-}"; shift 2 ;;
          --search-dirs) search_dirs="${2:-}"; shift 2 ;;
          --help|-h)
            print_usage
            exit 0
            ;;
          *) die "Unknown find-godaddy-certs option: $1" ;;
        esac
      done
      find_godaddy_certs "$domain" "$search_dirs"
      ;;
    migrate)
      migrate_inventory "$@"
      ;;
    delete)
      delete_site "$@"
      ;;
    menu)
      IN_MENU_MODE=1
      run_menu
      ;;
    help|-h|--help)
      print_usage
      ;;
    *)
      die "Unknown command: ${cmd}. Run: $0 help"
      ;;
  esac
}

main "$@"
