#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
TRANSPORT_LIB="${SCRIPT_DIR}/lib/transport.sh"
DEFAULT_ENV_FILE="${SCRIPT_DIR}/enterprise.env"
DEFAULT_REMOTE_DIR="/opt/security-center/enterprise"

# shellcheck disable=SC1090
source "${TRANSPORT_LIB}"

usage() {
  cat <<'EOF'
Issue or renew a Let's Encrypt certificate for the enterprise EC2 host.

The current production path uses host-mounted certificate files consumed by
the nginx container. This helper drives certbot on the remote EC2 host.

Usage:
  ./scripts/enterprise/cert-enterprise-letsencrypt.sh \
    --env-file ./scripts/enterprise/enterprise.env \
    --instance-id i-0123456789abcdef0

Options:
  --env-file <path>        Enterprise env file. Default: scripts/enterprise/enterprise.env
  --transport <mode>       Optional. ssm (default) or ssh.
  --instance-id <id>       Required for ssm unless provided via env.
  --host <user@host>       Required for ssh.
  --identity-file <path>   Optional. SSH private key for ssh mode.
  --remote-dir <path>      Optional. Default: /opt/security-center/enterprise
  --region <name>          AWS region for ssm mode.
  --profile <name>         AWS CLI profile override.
  --dry-run                Print commands without changing the remote host.
  --help                   Show this help.
EOF
}

info() { echo "INFO: $*"; }
warn() { echo "WARN: $*" >&2; }
die() { echo "ERROR: $*" >&2; exit 1; }

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

resolve_path() {
  local path="$1"
  if [[ "$path" = /* ]]; then
    printf '%s' "$path"
    return 0
  fi
  if [[ -e "$path" ]]; then
    printf '%s' "$path"
    return 0
  fi
  if [[ -e "${ROOT_DIR}/${path}" ]]; then
    printf '%s' "${ROOT_DIR}/${path}"
    return 0
  fi
  printf '%s' "$path"
}

trim() {
  local value="${1:-}"
  value="${value#"${value%%[![:space:]]*}"}"
  value="${value%"${value##*[![:space:]]}"}"
  printf '%s' "$value"
}

get_env_value() {
  local env_file="$1"
  local key="$2"
  [[ -f "${env_file}" ]] || return 0
  python3 - "$env_file" "$key" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
key = sys.argv[2]
for raw in path.read_text().splitlines():
    line = raw.strip()
    if not line or line.startswith("#") or "=" not in line:
        continue
    lhs, rhs = line.split("=", 1)
    if lhs.strip() == key:
        print(rhs.strip())
        break
PY
}

resolve_host_ip() {
  local host="$1"
  local ip_part="${host#*@}"
  if [[ "${ip_part}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    printf '%s' "${ip_part}"
    return 0
  fi
  python3 - "$ip_part" <<'PY'
import socket
import sys

host = sys.argv[1]
try:
    print(socket.gethostbyname(host))
except OSError:
    pass
PY
}

resolve_domain_ips() {
  local domain="$1"
  python3 - "$domain" <<'PY'
import socket
import sys

domain = sys.argv[1]
try:
    _, _, addresses = socket.gethostbyname_ex(domain)
    for item in addresses:
        print(item)
except OSError:
    pass
PY
}

resolve_domain_ips_via_public_resolvers() {
  local domain="$1"
  command -v dig >/dev/null 2>&1 || return 0
  local resolver=""
  for resolver in 1.1.1.1 8.8.8.8; do
    dig +short "@${resolver}" "${domain}" 2>/dev/null || true
  done | awk 'NF' | sort -u
}

stop_nginx_if_present() {
  local rendered_dir="${REMOTE_DIR}/rendered"
  sc_transport_run "bash -lc 'if [[ -f \"${rendered_dir}/docker-compose.enterprise.yml\" && -f \"${rendered_dir}/.env.runtime\" ]]; then cd \"${rendered_dir}\" && docker compose --env-file .env.runtime -f docker-compose.enterprise.yml stop nginx >/dev/null 2>&1 || true; fi'"
}

start_nginx_if_present() {
  local rendered_dir="${REMOTE_DIR}/rendered"
  sc_transport_run "bash -lc 'if [[ -f \"${rendered_dir}/docker-compose.enterprise.yml\" && -f \"${rendered_dir}/.env.runtime\" ]]; then cd \"${rendered_dir}\" && docker compose --env-file .env.runtime -f docker-compose.enterprise.yml up -d nginx >/dev/null 2>&1 || true; fi'"
}

install_renewal_hooks() {
  local rendered_dir="${REMOTE_DIR}/rendered"
  local rendered_dir_q
  printf -v rendered_dir_q '%q' "${rendered_dir}"
  local remote_script
  remote_script="$(cat <<EOF
set -euo pipefail
compose_dir=${rendered_dir_q}

# webroot mode never needs to stop nginx: drop the legacy stop/start hooks so a
# future renewal cannot take the site down. Keep only the deploy hook that
# reloads nginx after a successful renewal.
sudo mkdir -p \
  /etc/letsencrypt/renewal-hooks/pre \
  /etc/letsencrypt/renewal-hooks/post \
  /etc/letsencrypt/renewal-hooks/deploy

sudo rm -f \
  /etc/letsencrypt/renewal-hooks/pre/security-center-nginx-stop.sh \
  /etc/letsencrypt/renewal-hooks/post/security-center-nginx-start.sh

sudo tee /etc/letsencrypt/renewal-hooks/deploy/security-center-nginx-reload.sh >/dev/null <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
# After certbot renews /etc/letsencrypt/live/<host>/*.pem it updates the
# symlinks to point at the new archive files. A plain `nginx -s reload` is NOT
# enough: the nginx container bind-mounts those paths as file mounts, and
# Docker snapshots the symlink target at container creation time, so the
# container keeps serving the old cert even after the symlink moves.
# Force-recreate the nginx container so the bind mount re-resolves.
compose_dir=${rendered_dir}
if [[ -f "\$compose_dir/docker-compose.enterprise.yml" && -f "\$compose_dir/.env.runtime" ]]; then
  cd "\$compose_dir"
  docker compose --env-file .env.runtime -f docker-compose.enterprise.yml up -d --force-recreate nginx >/dev/null 2>&1 || true
fi
SCRIPT

sudo chmod 755 \
  /etc/letsencrypt/renewal-hooks/deploy/security-center-nginx-reload.sh

# Create an explicit renewal timer instead of relying on whichever unit the
# installed certbot package happens to ship (snap/pip/new-EPEL builds ship
# none, which silently left the previous timer disabled and the cert expired).
sudo tee /etc/systemd/system/certbot-renew.service >/dev/null <<'UNIT'
[Unit]
Description=Let's Encrypt certificate renewal (security-center)
Documentation=https://eff-certbot.readthedocs.io/

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet
UNIT

sudo tee /etc/systemd/system/certbot-renew.timer >/dev/null <<'UNIT'
[Unit]
Description=Daily Let's Encrypt renewal check (security-center)
Documentation=https://eff-certbot.readthedocs.io/

[Timer]
OnBootSec=30min
OnUnitInactiveSec=12h
RandomizedDelaySec=1h
Persistent=true
Unit=certbot-renew.service

[Install]
WantedBy=timers.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable certbot-renew.timer >/dev/null 2>&1 || true
sudo systemctl start certbot-renew.timer >/dev/null 2>&1 || true
EOF
)"
  info "installing certbot webroot renewal hook and explicit certbot-renew.timer"
  sc_transport_run "bash -lc $(printf '%q' "${remote_script}")"
}

verify_dns_points_to_host() {
  local remote_ip domain_ips public_domain_ips
  remote_ip="${SC_EC2_PUBLIC_IP:-}"
  if [[ -z "${remote_ip}" && "${TRANSPORT}" == "ssh" ]]; then
    remote_ip="$(resolve_host_ip "${REMOTE_HOST}")"
  fi
  if [[ -z "${remote_ip}" ]]; then
    if [[ "${DRY_RUN}" == "true" ]]; then
      warn "dry-run could not infer target public IP; DNS verification is informational only"
      info "would verify DNS ${SC_PUBLIC_HOSTNAME} points at the selected deployment target"
      return 0
    fi
    die "could not resolve target host IP for the selected deployment target"
  fi

  domain_ips="$(resolve_domain_ips "${SC_PUBLIC_HOSTNAME}")"
  if [[ -n "${domain_ips}" ]] && printf '%s\n' "${domain_ips}" | grep -Fxq "${remote_ip}"; then
    info "verifying DNS ${SC_PUBLIC_HOSTNAME} resolves to ${remote_ip}"
    return 0
  fi

  public_domain_ips="$(resolve_domain_ips_via_public_resolvers "${SC_PUBLIC_HOSTNAME}")"
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would verify DNS ${SC_PUBLIC_HOSTNAME} resolves to ${remote_ip}"
    return 0
  fi

  info "verifying DNS ${SC_PUBLIC_HOSTNAME} resolves to ${remote_ip}"
  if [[ -n "${public_domain_ips}" ]] && printf '%s\n' "${public_domain_ips}" | grep -Fxq "${remote_ip}"; then
    warn "system DNS still resolves ${SC_PUBLIC_HOSTNAME} differently, but public resolvers already point to ${remote_ip}; continuing"
    return 0
  fi

  if [[ -z "${domain_ips}" && -z "${public_domain_ips}" ]]; then
    die "public hostname does not resolve yet: ${SC_PUBLIC_HOSTNAME}"
  fi

  die "DNS for ${SC_PUBLIC_HOSTNAME} does not point to ${remote_ip}; fix the A record first"
}

issue_certificate() {
  local cert_path="/etc/letsencrypt/live/${SC_PUBLIC_HOSTNAME}"
  local renewal_conf="/etc/letsencrypt/renewal/${SC_PUBLIC_HOSTNAME}.conf"
  local webroot_dir="${SC_ACME_WEBROOT}"
  local lineage_exists="no"

  # Prepare the webroot on the host so nginx (via bind mount) can serve the
  # HTTP-01 challenge and certbot can write into it.
  info "ensuring webroot exists at ${webroot_dir}"
  if [[ "${DRY_RUN}" == "true" ]]; then
    echo "DRY-RUN remote: sudo mkdir -p '${webroot_dir}' && sudo chmod 755 '${webroot_dir}'"
  else
    sc_transport_run "sudo mkdir -p '${webroot_dir}' && sudo chmod 755 '${webroot_dir}'"
  fi

  if [[ "${DRY_RUN}" != "true" ]]; then
    if sc_transport_run "bash -lc 'test -d \"${cert_path}\" && test -f \"${renewal_conf}\" && echo yes || echo no'" 2>/dev/null | grep -Fxq "yes"; then
      lineage_exists="yes"
    fi
  else
    echo "DRY-RUN remote: would check lineage at ${cert_path} / ${renewal_conf}"
  fi

  info "requesting or renewing Let's Encrypt certificate for ${SC_PUBLIC_HOSTNAME} (lineage_exists=${lineage_exists})"

  if [[ "${DRY_RUN}" == "true" ]]; then
    if [[ "${lineage_exists}" == "yes" ]]; then
      echo "DRY-RUN remote: sudo certbot certonly --webroot -w '${webroot_dir}' --non-interactive --agree-tos --keep-until-expiring -m '${SC_TLS_ACME_EMAIL}' -d '${SC_PUBLIC_HOSTNAME}'"
    else
      echo "DRY-RUN remote: sudo certbot certonly --standalone --non-interactive --agree-tos --keep-until-expiring -m '${SC_TLS_ACME_EMAIL}' -d '${SC_PUBLIC_HOSTNAME}'"
    fi
  elif [[ "${lineage_exists}" == "yes" ]]; then
    # Existing lineage (e.g. the currently-expired host): issue via webroot.
    # Re-issue also rewrites renewal/<host>.conf to authenticator = webroot,
    # so future `certbot renew` runs no longer need to stop nginx.
    sc_transport_run "sudo certbot certonly --webroot -w '${webroot_dir}' --non-interactive --agree-tos --keep-until-expiring -m '${SC_TLS_ACME_EMAIL}' -d '${SC_PUBLIC_HOSTNAME}'"
  else
    # Fresh bootstrap: nginx is not up yet, port 80 is free. Use standalone so
    # there is no chicken-and-egg with nginx needing a cert to start. The
    # bootstrap flow then flips the authenticator to webroot once nginx is up.
    stop_nginx_if_present
    sc_transport_run "sudo certbot certonly --standalone --non-interactive --agree-tos --keep-until-expiring -m '${SC_TLS_ACME_EMAIL}' -d '${SC_PUBLIC_HOSTNAME}'"
    start_nginx_if_present
  fi

  if [[ "${DRY_RUN}" != "true" ]]; then
    info "verifying certificate files on the remote host"
    sc_transport_run "test -f '${cert_path}/fullchain.pem'"
    sc_transport_run "test -f '${cert_path}/privkey.pem'"
  fi
  install_renewal_hooks
}

ENV_FILE="${DEFAULT_ENV_FILE}"
TRANSPORT="ssm"
INSTANCE_ID=""
REMOTE_HOST=""
IDENTITY_FILE=""
REMOTE_DIR="${DEFAULT_REMOTE_DIR}"
AWS_REGION_NAME="${AWS_REGION:-${AWS_DEFAULT_REGION:-}}"
AWS_PROFILE_NAME="${AWS_PROFILE:-}"
DRY_RUN="false"

while (($# > 0)); do
  case "$1" in
    --env-file)
      ENV_FILE="$2"
      shift 2
      ;;
    --transport)
      TRANSPORT="$2"
      shift 2
      ;;
    --instance-id)
      INSTANCE_ID="$2"
      shift 2
      ;;
    --host)
      REMOTE_HOST="$2"
      shift 2
      ;;
    --identity-file)
      IDENTITY_FILE="$(resolve_path "$2")"
      shift 2
      ;;
    --remote-dir)
      REMOTE_DIR="$2"
      shift 2
      ;;
    --region)
      AWS_REGION_NAME="$2"
      shift 2
      ;;
    --profile)
      AWS_PROFILE_NAME="$2"
      shift 2
      ;;
    --dry-run)
      DRY_RUN="true"
      shift
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      die "unknown argument: $1"
      ;;
  esac
done

ENV_FILE="$(resolve_path "${ENV_FILE}")"
[[ -f "${ENV_FILE}" ]] || die "env file not found: ${ENV_FILE}"

SC_PUBLIC_HOSTNAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME")")"
SC_TLS_ACME_EMAIL="$(trim "$(get_env_value "${ENV_FILE}" "SC_TLS_ACME_EMAIL")")"
SC_EC2_PUBLIC_IP="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_PUBLIC_IP")")"
SC_ACME_WEBROOT="$(trim "$(get_env_value "${ENV_FILE}" "SC_ACME_WEBROOT")")"
SC_ACME_WEBROOT="${SC_ACME_WEBROOT:-/var/www/certbot}"

[[ -n "${SC_PUBLIC_HOSTNAME}" ]] || die "SC_PUBLIC_HOSTNAME is required in ${ENV_FILE}"
[[ -n "${SC_TLS_ACME_EMAIL}" ]] || die "SC_TLS_ACME_EMAIL is required in ${ENV_FILE} for Let's Encrypt automation"

require_bin python3
case "${TRANSPORT}" in
  ssm)
    [[ -n "${INSTANCE_ID}" ]] || INSTANCE_ID="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_ID")")"
    [[ -n "${INSTANCE_ID}" ]] || die "--instance-id is required for --transport ssm"
    [[ -n "${AWS_REGION_NAME}" ]] || AWS_REGION_NAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_AWS_REGION")")"
    [[ -n "${AWS_REGION_NAME}" ]] || die "--region is required for --transport ssm"
    ;;
  ssh)
    [[ -n "${REMOTE_HOST}" ]] || die "--host is required for --transport ssh"
    require_bin ssh
    ;;
  *)
    die "unsupported transport: ${TRANSPORT}"
    ;;
esac
if [[ -n "${IDENTITY_FILE}" ]]; then
  [[ -f "${IDENTITY_FILE}" ]] || die "identity file not found: ${IDENTITY_FILE}"
fi

SC_TRANSPORT_MODE="${TRANSPORT}"
SC_TRANSPORT_DRY_RUN="${DRY_RUN}"
SC_TRANSPORT_REMOTE_HOST="${REMOTE_HOST}"
SC_TRANSPORT_INSTANCE_ID="${INSTANCE_ID}"
SC_TRANSPORT_IDENTITY_FILE="${IDENTITY_FILE}"
SC_TRANSPORT_AWS_REGION="${AWS_REGION_NAME}"
SC_TRANSPORT_AWS_PROFILE="${AWS_PROFILE_NAME}"
sc_transport_init

verify_dns_points_to_host
issue_certificate
info "Let's Encrypt certificate is ready for ${SC_PUBLIC_HOSTNAME}"
