#!/usr/bin/env bash
#
# makecert.sh — macOS equivalent of makecert.bat
#
# Generates a self-signed certificate (key/csr/crt/cer/pem), fetches the
# vault-site-wizard PowerShell script, scaffolds config.json, and ensures
# PowerShell 7 is installed.
#
# Written for the stock macOS shell (bash 3.2). No Homebrew required.
#
# Usage:
#   chmod +x makecert.sh && ./makecert.sh
#
# Environment overrides:
#   SITE            Skip the site-name prompt
#   DAYS            Skip the validity prompt
#   WIZARD_URL      Override the wizard download URL
#   NONINTERACTIVE  Set to 1 to accept all defaults without prompting
#

set -uo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
cd "$SCRIPT_DIR" || { echo "[ERROR] Cannot cd to script directory." >&2; exit 1; }

# ------------------------------------------------------------
# Constants
# ------------------------------------------------------------
readonly DEFAULT_SITE="XQ_Offloading"
readonly DEFAULT_DAYS="1095"
# The bash wizard is the default: it needs nothing beyond what macOS ships.
# Set WIZARD_FLAVOR=powershell for the PnP.PowerShell route instead.
readonly WIZARD_FLAVOR="${WIZARD_FLAVOR:-bash}"
readonly CONFIG_FILE="config.json"
readonly PWSH_RELEASE_API="https://api.github.com/repos/PowerShell/PowerShell/releases/latest"

case "$WIZARD_FLAVOR" in
  bash)
    readonly WIZARD_FILE="vault-site-wizard.sh"
    readonly WIZARD_URL_MACOS="https://content.xqmsg.cloud/vault/vault-site-wizard.sh"
    readonly WIZARD_URL_LEGACY=""
    ;;
  powershell)
    readonly WIZARD_FILE="vault-site-wizard.ps1"
    readonly WIZARD_URL_MACOS="https://content.xqmsg.cloud/vault-site-wizard-macos.ps1"
    readonly WIZARD_URL_LEGACY="https://content.xqmsg.cloud/vault-site-wizard.ps1"
    ;;
  *)
    printf '[ERROR] WIZARD_FLAVOR must be "bash" or "powershell", got "%s".\n' "$WIZARD_FLAVOR" >&2
    exit 1
    ;;
esac

OPENSSL=""
EXT_FILE=""

# ------------------------------------------------------------
# Helpers
# ------------------------------------------------------------
info()  { printf '%s\n' "$*"; }
warn()  { printf '[WARN] %s\n' "$*" >&2; }
err()   { printf '[ERROR] %s\n' "$*" >&2; }
die()   { err "$*"; exit 1; }

cleanup() { [ -n "$EXT_FILE" ] && rm -f "$EXT_FILE"; }
trap cleanup EXIT

# ask <prompt> <default> -> echoes answer
ask() {
  local prompt="$1" default="$2" reply=""
  if [ "${NONINTERACTIVE:-0}" = "1" ]; then
    printf '%s\n' "$default"
    return 0
  fi
  printf '%s (default: %s)\n' "$prompt" "$default" >&2
  printf 'Press ENTER to use the default, or type a custom value.\n' >&2
  printf '> ' >&2
  IFS= read -r reply || reply=""
  printf '\n' >&2
  if [ -z "$reply" ]; then printf '%s\n' "$default"; else printf '%s\n' "$reply"; fi
}

# confirm <prompt> -> 0 if yes
confirm() {
  local reply=""
  [ "${NONINTERACTIVE:-0}" = "1" ] && return 0
  printf '%s [y/N] ' "$1" >&2
  IFS= read -r reply || reply=""
  case "$reply" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac
}

# ------------------------------------------------------------
# 1) Locate OpenSSL
#
# macOS ships LibreSSL at /usr/bin/openssl, which supports every command this
# script needs. A Homebrew openssl@3 is preferred when present, but nothing is
# installed (and therefore nothing needs uninstalling, unlike the Windows path).
# ------------------------------------------------------------
find_openssl() {
  local candidates="" c brew_prefix=""

  if command -v brew >/dev/null 2>&1; then
    brew_prefix="$(brew --prefix openssl@3 2>/dev/null || true)"
    [ -n "$brew_prefix" ] && candidates="$brew_prefix/bin/openssl"
  fi
  candidates="$candidates
/opt/homebrew/opt/openssl@3/bin/openssl
/usr/local/opt/openssl@3/bin/openssl
$(command -v openssl 2>/dev/null || true)
/usr/bin/openssl"

  while IFS= read -r c; do
    [ -z "$c" ] && continue
    if [ -x "$c" ]; then OPENSSL="$c"; return 0; fi
  done <<EOF
$candidates
EOF
  return 1
}

# ------------------------------------------------------------
# 2) Certificate generation
# ------------------------------------------------------------
sanitize_name() {
  # Strip anything that is unsafe in a filename or a DNS name.
  printf '%s' "$1" | tr -cd 'A-Za-z0-9._-'
}

generate_cert() {
  local name="$1" cn="$2" days="$3"

  info "Generating RSA key..."
  "$OPENSSL" genrsa -out "$name.key" 2048 || die "openssl genrsa failed."
  chmod 600 "$name.key"

  info "Creating CSR..."
  "$OPENSSL" req -new -key "$name.key" -out "$name.csr" -subj "/CN=$cn" \
    || die "openssl req failed."

  EXT_FILE="$(mktemp "${TMPDIR:-/tmp}/xq_ext.XXXXXX")" || die "mktemp failed."
  cat > "$EXT_FILE" <<EOF
[v3_req]
subjectAltName=DNS:$cn
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth,clientAuth
EOF

  info "Creating self-signed certificate ($days days) with SAN..."
  "$OPENSSL" x509 -req -in "$name.csr" -signkey "$name.key" -out "$name.crt" \
    -days "$days" -extfile "$EXT_FILE" -extensions v3_req \
    || die "openssl x509 failed."

  rm -f "$EXT_FILE"; EXT_FILE=""

  info "Exporting DER (.cer)..."
  "$OPENSSL" x509 -in "$name.crt" -outform DER -out "$name.cer" \
    || die "openssl x509 -outform DER failed."

  info "Creating PEM (cert+key)..."
  cat "$name.crt" "$name.key" > "$name.pem" || die "Could not write $name.pem."
  chmod 600 "$name.pem"

  info "Done for $cn ($days days)"
}

# ------------------------------------------------------------
# 3) Wizard download
# ------------------------------------------------------------
download_wizard() {
  local url="" urls=""

  # A wizard shipped alongside this script is authoritative; never silently
  # replace it. Unattended runs keep what is already there.
  if [ -f "$WIZARD_FILE" ]; then
    if [ "${NONINTERACTIVE:-0}" = "1" ]; then
      info "Found existing $WIZARD_FILE — keeping it."
      [ "$WIZARD_FLAVOR" = "bash" ] && chmod +x "$WIZARD_FILE"
      return 0
    fi
    info "Found existing $WIZARD_FILE."
    if ! confirm "Re-download and overwrite it?"; then
      info "Keeping the existing $WIZARD_FILE."
      [ "$WIZARD_FLAVOR" = "bash" ] && chmod +x "$WIZARD_FILE"
      return 0
    fi
  fi

  if [ -n "${WIZARD_URL:-}" ]; then
    urls="$WIZARD_URL"
  else
    urls="$WIZARD_URL_MACOS
$WIZARD_URL_LEGACY"
  fi

  while IFS= read -r url; do
    [ -z "$url" ] && continue
    info "Downloading $url ..."
    if curl -fsSL --retry 2 --connect-timeout 15 -o "$WIZARD_FILE.tmp" "$url"; then
      mv -f "$WIZARD_FILE.tmp" "$WIZARD_FILE"
      [ "$WIZARD_FLAVOR" = "bash" ] && chmod +x "$WIZARD_FILE"
      info "Successfully downloaded $WIZARD_FILE"
      if [ -n "$WIZARD_URL_LEGACY" ] && [ "$url" = "$WIZARD_URL_LEGACY" ]; then
        warn "This is the Windows build of the wizard. Use vault-site-wizard-macos.ps1 on macOS."
      fi
      return 0
    fi
    rm -f "$WIZARD_FILE.tmp"
    warn "Could not download $url"
  done <<EOF
$urls
EOF

  warn "Could not download $WIZARD_FILE. Place it next to this script manually."
  return 1
}

# ------------------------------------------------------------
# 4) config.json scaffold
#
# Unlike the .bat, an existing config.json is never clobbered — it is the one
# file the operator hand-edits.
# ------------------------------------------------------------
write_config() {
  if [ -f "$CONFIG_FILE" ]; then
    warn "$CONFIG_FILE already exists — leaving it untouched."
    return 0
  fi

  info "Creating $CONFIG_FILE..."
  cat > "$CONFIG_FILE" <<'EOF'
{
  "PnPAppClientId": "CLIENT_ID_FOR_PNP_APP",
  "PnPAppTenantId": "TENANT_ID_FOR_PNP_APP",
  "VaultAppClientId": "CLIENT_ID_FOR_YOUR_OFFLOADING_APP",
  "VaultAppName": "YOUR APP NAME",
  "Sites": [
    {
      "Url": "https://YOURDOMAIN.sharepoint.com/sites/EMAIL_OFFLOADING_URL",
      "Permission": "Write",
      "Action": "Grant"
    },
    {
      "Url": "https://YOURDOMAIN.sharepoint.com/sites/VAULT_SHARING_URL",
      "Permission": "Write",
      "Action": "Grant"
    }
  ]
}
EOF

  if [ -f "$CONFIG_FILE" ]; then
    info "Successfully created $CONFIG_FILE"
  else
    err "Failed to create $CONFIG_FILE"
    return 1
  fi
}

# ------------------------------------------------------------
# 5) PowerShell 7
# ------------------------------------------------------------
pwsh_arch() {
  case "$(uname -m)" in
    arm64)  printf 'arm64' ;;
    x86_64) printf 'x64' ;;
    *)      printf '' ;;
  esac
}

install_pwsh_brew() {
  command -v brew >/dev/null 2>&1 || return 1
  info "Installing PowerShell 7 via Homebrew (you will be asked for your password)..."
  brew install --cask powershell
}

install_pwsh_pkg() {
  local arch url pkg
  arch="$(pwsh_arch)"
  [ -z "$arch" ] && { warn "Unsupported CPU architecture: $(uname -m)"; return 1; }

  info "Looking up the latest PowerShell release..."
  url="$(curl -fsSL --connect-timeout 15 "$PWSH_RELEASE_API" 2>/dev/null \
        | grep -o "https://[^\"]*osx-$arch\.pkg" | head -1)"
  [ -z "$url" ] && { warn "Could not determine the PowerShell download URL."; return 1; }

  pkg="$(mktemp "${TMPDIR:-/tmp}/powershell.XXXXXX").pkg"
  info "Downloading $url ..."
  curl -fsSL --retry 2 -o "$pkg" "$url" || { rm -f "$pkg"; warn "Download failed."; return 1; }

  info "Installing PowerShell 7 (administrator password required)..."
  sudo installer -pkg "$pkg" -target / || { rm -f "$pkg"; warn "installer failed."; return 1; }
  rm -f "$pkg"
  return 0
}

ensure_pwsh() {
  info ""
  info "Checking for PowerShell 7..."

  if command -v pwsh >/dev/null 2>&1; then
    info "PowerShell 7 is already installed ($(pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null))."
    return 0
  fi

  warn "PowerShell 7 is not installed."
  info "PowerShell 7 is required to run $WIZARD_FILE"
  info ""
  if ! confirm "Install PowerShell 7 now?"; then
    warn "Skipping PowerShell 7 install. Install it later from https://aka.ms/powershell"
    return 1
  fi

  install_pwsh_brew || install_pwsh_pkg || {
    err "Failed to install PowerShell 7."
    info "Install it manually from: https://aka.ms/powershell"
    return 1
  }

  # /usr/local/bin is where both installers drop the pwsh symlink.
  if ! command -v pwsh >/dev/null 2>&1; then
    PATH="/usr/local/bin:$PATH"
    export PATH
    hash -r 2>/dev/null || true
  fi

  if command -v pwsh >/dev/null 2>&1; then
    info "Successfully installed PowerShell 7!"
    return 0
  fi

  warn "PowerShell 7 was installed but is not on PATH. Open a new terminal and retry."
  return 1
}

# ------------------------------------------------------------
# Main
# ------------------------------------------------------------
main() {
  info "============================================================"
  info "  [Self-Signed Certificate Generator — macOS]"
  info "  Initializing environment... please wait."
  info "============================================================"
  info ""

  find_openssl || die "No usable openssl found. Install one with: brew install openssl@3"
  info "Using openssl: $OPENSSL ($("$OPENSSL" version 2>/dev/null))"
  info ""

  local site days name cn
  site="${SITE:-$(ask 'Base site name' "$DEFAULT_SITE")}"
  site="$(sanitize_name "$site")"
  [ -z "$site" ] && { warn "Site name was empty after sanitizing. Using default."; site="$DEFAULT_SITE"; }

  days="${DAYS:-$(ask 'Enter certificate validity in days' "$DEFAULT_DAYS")}"
  if ! printf '%s' "$days" | grep -Eq '^[0-9]+$'; then
    warn "Invalid number. Using default $DEFAULT_DAYS."
    days="$DEFAULT_DAYS"
  fi
  info "Using $days day validity."
  info ""

  name="$site"
  cn="developer.$site.com"

  if [ -f "$name.key" ]; then
    warn "$name.key already exists. Regenerating replaces the existing private key."
    confirm "Overwrite $name.key and the related certificate files?" \
      || die "Aborted at the user's request."
  fi

  generate_cert "$name" "$cn" "$days"

  info ""
  download_wizard || true

  info ""
  write_config || true

  # PowerShell is only a prerequisite for the .ps1 flavour of the wizard.
  if [ "$WIZARD_FLAVOR" = "powershell" ]; then
    ensure_pwsh || true
  fi

  info ""
  info "============================================================"
  info "  Setup complete!"
  info "  Files created:"
  info "  - Certificate files: $name.key, $name.csr, $name.crt, $name.cer, $name.pem"
  info "  - $WIZARD_FILE"
  info "  - $CONFIG_FILE"
  info ""
  info "  Next steps:"
  info "  1. Edit $CONFIG_FILE with your Entra ID (Azure AD) app details"
  if [ "$WIZARD_FLAVOR" = "bash" ]; then
    info "  2. Preview:  ./$WIZARD_FILE --config ./$CONFIG_FILE --dry-run"
    info "  3. Apply:    ./$WIZARD_FILE --config ./$CONFIG_FILE"
  else
    info "  2. Run: pwsh -File $WIZARD_FILE -ConfigPath ./$CONFIG_FILE"
  fi
  info "============================================================"
}

main "$@"
