#!/usr/bin/env bash
# unoverse ground — prefill terraform.tfvars for your cloud ground
#
# The tfvars file is the ONE file a developer fills before `terraform apply`
# renders the complete .env.production. Most of its values are discoverable
# from the cloud CLI already on their machine, so this command discovers them
# and writes the file, leaving clearly marked FILL_ME lines for the handful
# only the developer knows (domain, IdP, service keys).
#
# Never clobbers: an existing terraform.tfvars is left alone.

_ground_my_ip() {
  curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://ifconfig.me 2>/dev/null
}

# A GROUND WRITES SECRETS TO DISK, so the ignore rule ships with the ground itself.
#
# terraform.tfvars holds the database URL, the registry token and every service key; the
# state file holds those plus everything Terraform generated, in plain text. The platform
# repo has ignored them since the grounds landed. A universe scaffolded by the CLI did not,
# because its .gitignore comes from the starter — which only changes when the starter is
# re-synced and pushed, long after somebody's first `git init && git add .`.
#
# Fixing it here means the rule exists the moment the file it protects does, on every
# universe, without waiting for a starter release. Idempotent: it appends only what is
# missing, and never rewrites a line the developer put there.
_ground_protect_secrets() {
  local gi="$ROOT/.gitignore" rule
  [ -f "$gi" ] || : > "$gi"
  local added=0
  for rule in 'infra/**/.terraform/' 'infra/**/*.tfstate*' 'infra/**/terraform.tfvars' '.unoverse/'; do
    grep -qxF "$rule" "$gi" 2>/dev/null && continue
    [ "$added" = "0" ] && printf '\n# Terraform (infra/*) — state and vars carry secrets\n' >> "$gi"
    printf '%s\n' "$rule" >> "$gi"
    added=1
  done
  [ "$added" = "1" ] && ok "Added the terraform files to .gitignore ${DIM}(they hold your database URL and keys)${NC}"
  return 0
}

# THE UNIVERSE HAS A NAME, so ask for one. This file used to write name = "universe-poc"
# as a literal, so every universe on every account carried the same placeholder — it names
# the droplet, the cache, the load balancer and the cloud project, and the developer first
# meets it in their provider dashboard, after the bill has started. A generated file is not
# a question: nobody edits it before running deploy.
#
# Sets GROUND_NAME (and GROUND_REGION when a default is passed). Both are sanitised to what
# cloud resource names allow, so a typed "My POC" cannot fail an apply five minutes in.
_ground_identity() {
  local valid="$1" suggest
  suggest=$(basename "$ROOT" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//')
  suggest="${suggest:-universe}"
  echo ""
  read -r -p "  Name this universe [$suggest]: " GROUND_NAME
  GROUND_NAME=$(echo "${GROUND_NAME:-$suggest}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//')
  GROUND_NAME="${GROUND_NAME:-$suggest}"

  # NO DEFAULT REGION. A default is a choice made on the developer's behalf, and the
  # previous one was mine: `lon1`, written straight into the file, which is how a universe
  # meant for Singapore was built in London. There is no neutral region to fall back to, so
  # this asks until it gets a real answer and checks it against the provider's own list —
  # a typo here surfaces as a five-minute apply that fails on the last resource.
  # "-" means the region is not this ground's to ask: AWS reads it from the developer's own
  # CLI configuration, which is already their answer.
  GROUND_REGION=""
  [ "$valid" = "-" ] && return 0
  while [ -z "$GROUND_REGION" ]; do
    read -r -p "  Which region? " GROUND_REGION
    GROUND_REGION=$(echo "$GROUND_REGION" | tr -d '[:space:]')
    # Validate only against a list we actually retrieved. An empty list means the lookup
    # failed, not that every region is wrong.
    if [ -n "$GROUND_REGION" ] && [ -n "$valid" ] && ! echo "$valid" | tr ' ' '\n' | grep -qx "$GROUND_REGION"; then
      warn "$GROUND_REGION is not a region in your account. Pick one from the list above"
      GROUND_REGION=""
    fi
  done
}

_ground_do() {
  local dir="$ROOT/infra/digitalocean"
  local out="$dir/terraform.tfvars"

  if ! command -v doctl >/dev/null 2>&1; then
    # Offer to install rather than sending the developer away with homework: brew is
    # already on most Macs that got this far. Their yes, their machine, our command.
    if command -v brew >/dev/null 2>&1; then
      local REPLY
      read -r -p "  doctl (the DigitalOcean CLI) is needed. Install it now with brew? [Y/n] " REPLY
      if [[ ! "$REPLY" =~ ^[Nn]$ ]]; then
        brew install doctl || { fail "brew install doctl failed"; return 1; }
        ok "doctl installed"
      else
        fail "doctl is needed to continue"
        info "  https://docs.digitalocean.com/reference/doctl/how-to/install/"
        return 1
      fi
    else
      fail "doctl is not installed"
      info "  brew install doctl        (macOS)"
      info "  https://docs.digitalocean.com/reference/doctl/how-to/install/"
      info "Then authenticate: doctl auth init"
      return 1
    fi
  fi
  if ! doctl account get >/dev/null 2>&1; then
    # Authenticate RIGHT HERE and keep going: doctl auth init is interactive anyway,
    # so there is nothing to leave the flow for. The token comes from
    # cloud.digitalocean.com/account/api (Generate New Token, read and write).
    echo ""
    # ONE prompt, ours. doctl auth init prints its own paragraph repeating the link,
    # so the token is taken here and handed over silently (-t): one message, one paste.
    info "A DigitalOcean API token connects your account. Generate one here:"
    echo ""
    echo -e "      ${CYAN}https://cloud.digitalocean.com/account/api/tokens${NC}"
    echo ""
    # Full Access, stated plainly: this token CREATES infrastructure (droplet, load
    # balancer, DNS, Postgres, Redis). Read Only cannot, and enumerating custom scopes
    # for that list is homework that breaks when the ground grows.
    echo -e "  ${DIM}Scope: choose ${NC}${BOLD}Full Access${NC}${DIM}. This token creates your server, database and networking.${NC}"
    echo -e "  ${DIM}It stays on this machine; developers never receive it.${NC}"
    echo ""
    local DO_TOKEN
    read -r -s -p "  Paste the token (hidden): " DO_TOKEN
    echo ""
    if [ -z "$DO_TOKEN" ] || ! doctl auth init -t "$DO_TOKEN" >/dev/null 2>&1 || ! doctl account get >/dev/null 2>&1; then
      fail "that token did not authenticate"
      return 1
    fi
    ok "DigitalOcean connected"
  fi
  ok "doctl authenticated"

  local ip keys first_key pg_clusters pg_line
  ip=$(_ground_my_ip)
  [ -n "$ip" ] && ok "your IP: $ip" || warn "could not discover your IP (fill admin_cidr yourself)"

  keys=$(doctl compute ssh-key list --format Name --no-header 2>/dev/null)
  first_key=$(echo "$keys" | head -1)
  if [ -n "$first_key" ]; then
    ok "SSH key: $first_key$( [ "$(echo "$keys" | wc -l | xargs)" -gt 1 ] && echo " (of $(echo "$keys" | wc -l | xargs) — alternatives listed in the file)")"
  else
    warn "no SSH keys in the DO account. Add one first (doctl compute ssh-key import)"
  fi

  pg_clusters=$(doctl databases list --format Name,Engine --no-header 2>/dev/null | awk '$2=="pg"{print $1}')
  if [ -n "$pg_clusters" ]; then
    pg_line="# existing_pg_cluster_name = \"$(echo "$pg_clusters" | head -1)\"   # found in your account — uncomment to REUSE it (Terraform adds this universe's own db/user/pool)"
    ok "existing Postgres cluster found: $(echo "$pg_clusters" | head -1) (reuse offered in the file)"
  else
    pg_line="# existing_pg_cluster_name = \"...\"   # none found in your account — leave unset to provision a fresh cluster"
    info "no existing Postgres cluster. Terraform will provision one"
  fi

  local regions
  regions=$(doctl compute region list --format Slug --no-header 2>/dev/null | tr -d '\r' | xargs)
  if [ -n "$regions" ]; then
    echo ""
    echo -e "  ${DIM}Regions available to you: $regions${NC}"
  fi
  _ground_protect_secrets
  _ground_identity "$regions"

  cat > "$out" <<EOF
# Generated by \`unoverse ground do\` on $(date +%Y-%m-%d). Discovered values are
# prefilled; every FILL_ME needs YOUR value before \`terraform apply\`.
# Token: set DIGITALOCEAN_TOKEN in the environment (preferred) or do_token here.

region       = "$GROUND_REGION"                    # doctl compute region list
name         = "$GROUND_NAME"
size         = "small"                   # small (POC) | medium | large
admin_cidr   = "${ip:-FILL_ME}${ip:+/32}"        # YOUR IP — SSH + Dozzle only
ssh_key_name = "${first_key:-FILL_ME}"   # must already exist in the DO account
$(echo "$keys" | tail -n +2 | sed 's/^/# ssh_key_name alternative: /')
# Domain is OPTIONAL — empty brings the universe up on the LB's IP over plain
# HTTP (terraform output api_url shows the address). Fill it in later and
# re-apply to upgrade in place to TLS at api.<domain>.
domain       = ""                        # empty = HTTP on the LB IP; "yourdomain.com" = TLS at api.yourdomain.com
manage_dns   = false                     # true only if the domain's DNS is on DO

# ⚠ With a domain set, the managed Let's Encrypt certificate requires the
# domain's DNS to be HOSTED on DigitalOcean (registrar can stay GoDaddy etc. —
# point the nameservers at ns1/ns2/ns3.digitalocean.com and add the domain
# under Networking). Then manage_dns = true also creates the records for you.

# POC ONLY: public Canvas at https://api.<domain>:3001 (a second port on the ONE LB).
# Add that URL to the IdP's allowed origins. Default false = admin-only.
canvas_public = true

# Auth — byo-oidc: roles/permissions live in YOUR IdP tenant.
auth_issuer    = "FILL_ME"               # e.g. https://your-tenant.auth0.com
auth_client_id = "FILL_ME"
auth_audience  = "gravity-api"

# Service secrets — with these set, the rendered .env.production is COMPLETE.
docr_token     = "FILL_ME"               # read-only registry token from your Unoverse admin
openai_api_key = "FILL_ME"
# hyperbrowser_api_key = ""              # optional — page intelligence

# Postgres — leave unset to provision a fresh cluster:
$pg_line
#   Or a fully external database, used verbatim:
# byo_postgres_url = "postgresql://user:pass@host:5432/db?sslmode=require"

# Redis: always provisioned by Terraform (Managed Redis, TLS). Not configurable.

# The rendered output (terraform output -raw env_production > ../../.env.production)
# IS your universe's secrets, including the MASTER KEY (CREDENTIAL_ENCRYPTION_KEY)
# that locks every credential users save in the platform. If it's lost, no backup
# can bring them back. Never commit it; keep a safe copy with your database backups.
EOF

  # Blank alternative-keys line when there is only one key.
  sed -i '' '/^# ssh_key_name alternative: *$/d' "$out" 2>/dev/null || sed -i '/^# ssh_key_name alternative: *$/d' "$out"
  ok "wrote infra/digitalocean/terraform.tfvars"
}

_ground_aws() {
  local dir="$ROOT/infra/aws"
  local out="$dir/terraform.tfvars"

  if ! command -v aws >/dev/null 2>&1; then
    fail "the AWS CLI is not installed"
    info "  brew install awscli       (macOS)"
    info "  https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
    info "Then authenticate: aws configure"
    return 1
  fi
  if ! aws sts get-caller-identity >/dev/null 2>&1; then
    fail "the AWS CLI is not authenticated"
    info "Run: aws configure   (or set AWS_PROFILE to a working profile)"
    return 1
  fi
  ok "AWS CLI authenticated ($(aws sts get-caller-identity --query Account --output text 2>/dev/null))"

  local ip region keys first_key zones zone_id zone_domain
  ip=$(_ground_my_ip)
  [ -n "$ip" ] && ok "your IP: $ip" || warn "could not discover your IP (fill admin_cidr yourself)"

  region=$(aws configure get region 2>/dev/null)
  [ -n "$region" ] && ok "region: $region" || { region="us-east-1"; info "no default region configured. Using us-east-1"; }

  # A KEY PAIR YOU CANNOT USE IS NOT A KEY PAIR. This took the FIRST key pair in the region,
  # which on a real account is whatever was created years ago in the console — its private
  # half is not on this laptop, so terraform applied happily and Ansible then died on
  # "Permission denied (publickey)" after eleven minutes of RDS provisioning.
  #
  # The operator's own key is the one that works, because deploying IS ssh-ing from here.
  # Upload it under the universe's name and use that: AWS lets terraform import a public
  # key, so nothing has to pre-exist and nothing has to be downloaded. Fall back to the
  # account's existing pairs only when this machine has no key at all.
  local pubkey=""
  for pubkey in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub" ""; do
    [ -n "$pubkey" ] && [ -f "$pubkey" ] && break
  done

  keys=$(aws ec2 describe-key-pairs --query 'KeyPairs[].KeyName' --output text 2>/dev/null | tr '\t' '\n')
  first_key=$(echo "$keys" | head -1)
  if [ -n "$pubkey" ]; then
    first_key=""                       # terraform creates the pair from operator_public_key
    ok "SSH key: your own ${DIM}($(basename "$pubkey") — uploaded as ${GROUND_NAME:-this universe}-operator)${NC}"
  elif [ -n "$first_key" ]; then
    warn "no SSH key on this machine — using the account's ${BOLD}$first_key${NC}"
    warn "the deploy will fail unless you hold its private half"
    ok "EC2 key pair: $first_key"
  else
    warn "no EC2 key pairs in $region. Create one first (aws ec2 create-key-pair)"
  fi

  # Exactly one hosted zone → we know the domain AND the zone id (auto DNS + cert validation).
  zones=$(aws route53 list-hosted-zones --query 'HostedZones[].[Id,Name]' --output text 2>/dev/null)
  if [ "$(echo "$zones" | grep -c .)" = "1" ]; then
    zone_id=$(echo "$zones" | awk '{print $1}' | sed 's|/hostedzone/||')
    zone_domain=$(echo "$zones" | awk '{print $2}' | sed 's/\.$//')
    ok "Route53 zone: $zone_domain ($zone_id): DNS + certificate validation will be automatic"
  elif [ -n "$zones" ]; then
    info "multiple Route53 zones. Pick one in the file (listed there)"
  fi

  # Region already came from the AWS CLI's own configuration, so only the name is asked.
  _ground_protect_secrets
  _ground_identity "-"

  cat > "$out" <<EOF
# Generated by \`unoverse ground aws\` on $(date +%Y-%m-%d). Discovered values are
# prefilled; every FILL_ME needs YOUR value before \`terraform apply\`.

region       = "$region"
name         = "$GROUND_NAME"
admin_cidr   = "${ip:-FILL_ME}${ip:+/32}"       # YOUR IP — the only SSH source
ssh_key_name = "${first_key}"  # empty = terraform uploads operator_public_key below
operator_public_key = "$( [ -n "$pubkey" ] && cat "$pubkey" )"   # your key: the deploy ssh-es from this machine
admin_email  = "FILL_ME"                # initial admin (Cognito user, all roles; invite emailed)
size         = "small"                  # small (POC) | medium | large
# Domain is OPTIONAL — empty brings the universe up on the ALB's DNS name over
# plain HTTP (terraform output api_url shows the address). Fill it in later and
# re-apply to upgrade in place to TLS at api.<domain>.
domain       = "${zone_domain:-}"        # empty = HTTP on the ALB DNS name; discovered from Route53 when present

# OPTIONAL: the domain's Route53 hosted zone id — set and Terraform creates the
# DNS records AND auto-validates the certificate.
$( if [ -n "$zone_id" ]; then echo "route53_zone_id = \"$zone_id\""; else echo "# route53_zone_id = \"Z0123456789ABC\""; fi )
$( [ -n "$zones" ] && [ -z "$zone_id" ] && echo "$zones" | awk '{gsub("/hostedzone/","",$1); print "# zone: " $2 " → " $1}' )

# POC ONLY: public Canvas at https://unoverse.<domain> — a host rule on the ONE
# ALB. Add the URL to oauth origins.
canvas_public = true

# Service secrets — with these set, the rendered .env.production is COMPLETE.
docr_token     = "FILL_ME"              # read-only registry token from your Unoverse admin
openai_api_key = "FILL_ME"
# hyperbrowser_api_key = ""             # optional — page intelligence

oauth_callback_urls = [
$( if [ -n "$zone_domain" ]; then echo "  \"https://canvas.$zone_domain\","; fi )
  "http://localhost:5173", # local Studio/Canvas dev against this universe
  # Add your Canvas URL here once a domain is set (https://canvas.<domain>).
  # (No need to list the CLI's login callback — main.tf always allows it.)
]

# RBAC roles (noun:verb). Each becomes a Cognito group; put a user in the group
# and the role rides their token.
roles = [
  "workflow:author",     # build and test workflows (builder gate — ENFORCED)
  "marketplace:publish", # publish assets to this universe (publish gate — ENFORCED)
  "workflow:promote",    # promote a draft to active / go-live (declared only)
]

# The rendered output (terraform output -raw env_production > ../../.env.production)
# IS your universe's secrets, including the MASTER KEY (CREDENTIAL_ENCRYPTION_KEY)
# that locks every credential users save in the platform. If it's lost, no backup
# can bring them back. Never commit it; keep a safe copy with your database backups.
EOF

  ok "wrote infra/aws/terraform.tfvars"
}

cmd_ground() {
  local which="${1:-}"

  # No argument: pick the ground by which CLI is present and authenticated.
  if [ -z "$which" ]; then
    local has_do=false has_aws=false
    command -v doctl >/dev/null 2>&1 && doctl account get >/dev/null 2>&1 && has_do=true
    command -v aws >/dev/null 2>&1 && aws sts get-caller-identity >/dev/null 2>&1 && has_aws=true
    if $has_do && $has_aws; then
      fail "both doctl and the AWS CLI are authenticated. Say which ground:"
      info "  unoverse ground do    or    unoverse ground aws"
      return 1
    elif $has_do; then which="do"
    elif $has_aws; then which="aws"
    else
      fail "no authenticated cloud CLI found"
      info "DigitalOcean:  brew install doctl && doctl auth init   → unoverse ground do"
      info "AWS:           brew install awscli && aws configure    → unoverse ground aws"
      return 1
    fi
  fi

  case "$which" in
    do|digitalocean)
      banner "Prefill your DigitalOcean ground"
      [ -f "$ROOT/infra/digitalocean/terraform.tfvars" ] && { fail "infra/digitalocean/terraform.tfvars already exists. Edit it, or delete it to regenerate"; return 1; }
      _ground_do || return 1
      echo ""
      : # no closing narration here: deploy owns the next-steps (one voice)
      ;;
    aws)
      banner "Prefill your AWS ground"
      [ -f "$ROOT/infra/aws/terraform.tfvars" ] && { fail "infra/aws/terraform.tfvars already exists. Edit it, or delete it to regenerate"; return 1; }
      _ground_aws || return 1
      echo ""
      : # no closing narration here: deploy owns the next-steps (one voice)
      ;;
    *)
      fail "unknown ground: $which (use: do | aws)"
      return 1
      ;;
  esac
}
