#!/bin/bash
set -e

# =============================================================================
# officer-setup — the platform, on a machine that is already provisioned
#
# The second half of the install. machine-setup/ brings a blank box up to a
# usable machine; this puts Officer on top of it.
#
# Run as root:  sudo scripts/setup/officer-setup.sh
# =============================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROGRESS_FILE="$SCRIPT_DIR/officer-setup/.setup-progress"

ONLY_STEP=""

# Kept before the loop consumes them. This script re-executes itself through sudo
# further down and was passing `"$@"`, which `shift` had already emptied — so
# `officer-setup.sh --only build` run as a normal user silently became a FULL run
# the moment it escalated. Nothing said so; the flag just stopped existing.
ORIGINAL_ARGS=(${@+"$@"})

while [[ $# -gt 0 ]]; do
  case "$1" in
    --only)
      ONLY_STEP="${2:-}"
      shift 2
      ;;
    --only=*)
      ONLY_STEP="${1#*=}"
      shift
      ;;
    # Set before lib/repo.sh is sourced below, which reads it as
    # `${OFFICER_REPO:-<default>}` — so this wins and an absent flag still defaults.
    --repo)
      [[ -n "${2:-}" ]] || {
        echo "--repo needs a URL" >&2
        exit 2
      }
      OFFICER_REPO="$2"
      shift 2
      ;;
    --repo=*)
      OFFICER_REPO="${1#*=}"
      shift
      ;;
    --unattended | -y)
      export UNATTENDED=1 ASSUME_YES=1
      shift
      ;;
    -l | --list)
      grep -oP '^step "\K[^"]+' "${BASH_SOURCE[0]}"
      exit 0
      ;;
    -h | --help)
      echo "usage: officer-setup.sh [--only <step>] [--list] [--repo <url>] [--unattended]"
      echo ""
      echo "  --only <step>  run one step; --list names them"
      echo "  --unattended   take the default for every question that has one (-y)"
      echo "  --repo <url>   clone from here instead of the default, which is a"
      echo "                 private Gitea over SSH and only authenticates on a"
      echo "                 machine whose key it already knows. Same as exporting"
      echo "                 OFFICER_REPO. Ignored once the repo is checked out."
      exit 0
      ;;
    *) echo "unknown option: $1" >&2 && exit 2 ;;
  esac
done
export OFFICER_REPO="${OFFICER_REPO:-}"

# shellcheck source=officer-setup/lib/base.sh
source "$SCRIPT_DIR/report.sh"
source "$SCRIPT_DIR/officer-setup/lib/base.sh"
# shellcheck source=officer-setup/lib/preflight.sh
source "$SCRIPT_DIR/officer-setup/lib/preflight.sh"
# shellcheck source=officer-setup/lib/repo.sh
source "$SCRIPT_DIR/officer-setup/lib/repo.sh"
# shellcheck source=officer-setup/lib/layout.sh
source "$SCRIPT_DIR/officer-setup/lib/layout.sh"
# shellcheck source=officer-setup/lib/postgres.sh
source "$SCRIPT_DIR/officer-setup/lib/postgres.sh"
# shellcheck source=officer-setup/lib/env.sh
source "$SCRIPT_DIR/officer-setup/lib/env.sh"
# shellcheck source=officer-setup/lib/secrets.sh
source "$SCRIPT_DIR/officer-setup/lib/secrets.sh"
# shellcheck source=officer-setup/lib/build.sh
source "$SCRIPT_DIR/officer-setup/lib/build.sh"
# shellcheck source=officer-setup/lib/services.sh
source "$SCRIPT_DIR/officer-setup/lib/services.sh"
# shellcheck source=officer-setup/lib/proxy.sh
source "$SCRIPT_DIR/officer-setup/lib/proxy.sh"

trap 'echo ""; echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}"; echo -e "${RED}║  OFFICER SETUP FAILED${NC}"; echo -e "${RED}║  Step: ${CURRENT_STEP:-unknown}${NC}"; echo -e "${RED}║  Line: $LINENO${NC}"; echo -e "${RED}║  Command: $BASH_COMMAND${NC}"; echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}"' ERR

# =============================================================================
# 1. Pre-flight
# =============================================================================

echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║          Officer Setup                           ║${NC}"
echo -e "${BOLD}╚══════════════════════════════════════════════════╝${NC}"

# ── Privileges: asked for, not demanded ──
#
# Run this as YOURSELF. It needs root on Linux, so it asks through sudo and
# re-executes itself rather than making you type it. Variables are passed to sudo
# by name rather than with -E, because `env_reset` is the sudoers default and
# strips the environment — which is how DATA_PATH was lost once already.
#
# macOS never escalates: Homebrew refuses to run as root, and the account running
# this IS the owner, so there is nothing to chown and nothing to drop to.
if [[ "$(uname -s)" == "Darwin" ]]; then
  if [[ "$EUID" -eq 0 ]]; then
    fail "Do not run this with sudo on macOS — run it as yourself."
  fi
elif [[ "$EUID" -ne 0 ]]; then
  command -v sudo >/dev/null 2>&1 || fail "This needs root and sudo is not installed — run it as root."
  echo ""
  echo "  This needs administrator rights. You will be asked for your password."
  echo ""
  exec sudo \
    OFFICER_ROOT="${OFFICER_ROOT:-}" \
    SETUP_USERNAME="${SETUP_USERNAME:-}" \
    MACHINE_ROLE="${MACHINE_ROLE:-}" \
    REPORT_FILE="${REPORT_FILE:-}" \
    UNATTENDED="${UNATTENDED:-}" \
    ASSUME_YES="${ASSUME_YES:-}" \
    OFFICER_REPO="${OFFICER_REPO:-}" \
    bash "$SCRIPT_DIR/officer-setup.sh" ${ORIGINAL_ARGS[@]+"${ORIGINAL_ARGS[@]}"}
fi

trap report_flush EXIT

# ── what machine-setup already established ──
echo ""
if load_machine_answers; then
  info "Read from machine-setup: ${MACHINE_ANSWERS}"
else
  warn "machine-setup has not run on this machine"
  echo "     That is fine if you provisioned it another way — the questions it"
  echo "     would have answered are asked below instead."
fi

# ── the account ──
#
# A remembered answer can go stale: the account it names may have been renamed or
# removed since machine-setup ran. That is a reason to ask again, not a reason to
# stop — so the remembered value is checked before it is trusted, and a bad one
# is reported and replaced rather than ending the run.
if [[ -n "$USERNAME" ]] && ! owner_exists; then
  warn "the remembered account '${USERNAME}' does not exist on this machine any more"
  USERNAME=""
fi

while [[ -z "$USERNAME" ]] || ! owner_exists; do
  echo ""
  info "Which account owns this Officer install?"
  echo "     Its files, its node_modules and its pm2 process list all belong to"
  echo "     this account rather than to root."
  echo ""
  ask_required USERNAME "Username" "${SUDO_USER:-}"
  owner_exists || warn "There is no account called '${USERNAME}' on this machine."
done

resolve_user_home

# ── where it goes ──
if [[ -z "$OFFICER_ROOT" ]]; then
  echo ""
  info "Where should Officer be installed?"
  echo "     One directory holding the app, its data, the item store and any"
  echo "     containers the app store provisions."
  echo ""
  ask_required OFFICER_ROOT "Path" "${USER_HOME}/officerdev"
fi
OFFICER_ROOT="${OFFICER_ROOT/#\~/$USER_HOME}"
[[ "$OFFICER_ROOT" == /* ]] || fail "That needs to be an absolute path — got '${OFFICER_ROOT}'"
OFFICER_ROOT="${OFFICER_ROOT%/}"

info "Account:  ${USERNAME} (home ${USER_HOME})"
info "Officer:  ${OFFICER_ROOT}"
[[ -n "$MACHINE_ROLE" ]] && info "Role:     ${MACHINE_ROLE}"

# ── recover what earlier runs already decided ──
#
# A skipped section leaves its variables unset, and later sections read them. On
# a resume that is every section before the one it stopped at, so Build announced
# "PUBLIC_URL <not set — run the Environment section first>" on a machine whose
# .env had been written twenty minutes earlier.
#
# Read back here, once, from the file that already holds the answers, rather than
# per-section — three variables cross a section boundary (ENV_PORT and
# ENV_PUBLIC_URL from Environment, POSTGRES_URL from Database) and the next one
# added would have to remember to do this again.
#
# Only fills what is EMPTY, so a variable passed in on the command line still
# wins, and a section that runs for real still overwrites it with its own answer.
if [[ -f "$(env_file)" ]]; then
  ENV_PORT="${ENV_PORT:-$(env_get PORT)}"
  ENV_PUBLIC_URL="${ENV_PUBLIC_URL:-$(env_get PUBLIC_URL)}"
  POSTGRES_URL="${POSTGRES_URL:-$(env_get POSTGRES_URL)}"
fi

# ── is the machine actually ready ──
#
# Checked and reported together. Finding out about a missing bun three sections
# in, after a repository has been cloned and a database started, is a worse way
# to learn it.
echo ""
info "What Officer needs from this machine"

mapfile -t MISSING < <(missing_tools)
mapfile -t MISSING_OPT < <(missing_optional_tools)

for t in "${REQUIRED_TOOLS[@]}"; do
  if command -v "$t" &>/dev/null; then
    printf '     %-6s %-10s %s\n' "$t" "ok" "$(tool_why "$t")"
  else
    printf '     %-6s %-10s %s\n' "$t" "MISSING" "$(tool_why "$t")"
  fi
done
for t in "${OPTIONAL_TOOLS[@]}"; do
  if command -v "$t" &>/dev/null; then
    printf '     %-6s %-10s %s\n' "$t" "ok" "$(tool_why "$t")"
  else
    printf '     %-6s %-10s %s\n' "$t" "absent" "$(tool_why "$t") — optional"
  fi
done

if ((${#MISSING[@]} > 0)); then
  echo ""
  fail "Missing: ${MISSING[*]}. Run scripts/setup/machine-setup/machine-setup.sh first, or install them yourself."
fi

if ((${#MISSING_OPT[@]} > 0)); then
  echo ""
  warn "No Docker. Postgres will have to be one you already run, and the app"
  echo "     store cannot provision anything until Docker is installed."
fi

if [[ -f "$PROGRESS_FILE" ]]; then
  echo ""
  info "Resuming — $(wc -l <"$PROGRESS_FILE") step(s) already done, and they will be skipped"
  echo "     To start over instead:  sudo rm ${PROGRESS_FILE}"
else
  echo ""
  echo "     This can be stopped at any point and run again later. Completed"
  echo "     steps are remembered and skipped."
fi

# =============================================================================
# 2. Layout
# =============================================================================
#
# Before the repository, because the repository is cloned into it.

report_section "Layout"
step "Layout"
if ! skip; then
  echo ""
  info "Layout — everything Officer owns, under one root"
  echo "     ${OFFICER_ROOT}/"
  echo "       platform/       the app"
  echo "       data/           managed homes, attachments, job logs"
  echo "       dockers/        anything the app store provisions"
  echo "       capabilities/   skills, tools, tasks, processes"
  echo ""
  echo "     Nothing here is configurable. The original asked separately for the"
  echo "     data directory and the item store, which were two answers that had"
  echo "     to agree with each other. One root now, and the rest follows."
  echo ""
  echo "     To put data/ on a bigger volume later, symlink it — that is a"
  echo "     decision about storage rather than about how Officer is laid out."

  mapfile -t WRONG_OWNER < <(layout_wrong_owner)
  if ((${#WRONG_OWNER[@]} > 0)); then
    echo ""
    warn "these exist but do not belong to ${USERNAME}:"
    printf '       %s\n' "${WRONG_OWNER[@]}"
    echo "     Everything that writes into them runs as ${USERNAME} — the platform"
    echo "     under pm2, the app store's compose files, the item store the agent"
    echo "     authors into. Left as they are, those writes fail in a way that"
    echo "     reads as a bug in the platform."
    if confirm "Give them to ${USERNAME}?"; then
      for d in "${WRONG_OWNER[@]}"; do chown -R "${USERNAME}:$(user_group)" "$d"; done
      ok "ownership corrected"
      SUMMARY+=("Layout: ownership corrected on ${#WRONG_OWNER[@]} directory(ies)")
    fi
  fi

  create_layout
  ok "layout in place under ${OFFICER_ROOT}"
  SUMMARY+=("Layout: ${OFFICER_ROOT} (data, dockers, capabilities)")
  step_ok
fi

# =============================================================================
# 3. Repository
# =============================================================================

report_section "Repository"
step "Repository"
if ! skip; then
  PLATFORM_DIR="$(platform_dir)"

  echo ""
  info "Repository — where the platform's code lives"
  echo "     path:      ${PLATFORM_DIR}"

  if repo_exists; then
    echo "     remote:    $(repo_remote)"
    echo "     branch:    $(repo_branch)"
    echo "     working:   $(repo_is_dirty && echo 'has uncommitted changes' || echo 'clean')"

    # Reported, never silently corrected. Repointing somebody's remote is a
    # decision about where their work goes, and this script is not entitled to
    # make it quietly.
    if [[ -n "$(repo_remote)" && "$(repo_remote)" != "$OFFICER_REPO" ]]; then
      echo ""
      warn "this checkout points somewhere other than ${OFFICER_REPO}"
      echo "     Left alone. To move it:"
      echo "       git -C ${PLATFORM_DIR} remote set-url origin ${OFFICER_REPO}"
    fi

    if repo_is_dirty; then
      echo ""
      echo "     not pulling — there are uncommitted changes here, and a pull"
      echo "     would either fail or bury them"
      SUMMARY+=("Repository: present at ${PLATFORM_DIR}, left alone (uncommitted changes)")
    elif confirm "Pull the latest changes?"; then
      if pull_repo; then
        ok "up to date on $(repo_branch)"
        SUMMARY+=("Repository: pulled, on $(repo_branch)")
      else
        # --ff-only, so this means the branch has diverged rather than that the
        # network failed. Saying which matters.
        warn "could not fast-forward — the local branch has diverged from the remote"
        ERRORS+=("Repository: pull refused, branch diverged")
        SUMMARY+=("Repository: present, pull refused (diverged)")
      fi
    else
      SUMMARY+=("Repository: present at ${PLATFORM_DIR}")
    fi

  else
    echo "     nothing there yet"
    echo ""
    info "Clone from ${OFFICER_REPO}?"
    echo "     Cloned as ${USERNAME}, not as root — a repository owned by root is"
    echo "     one you cannot pull, commit in, or install into."

    CLONE_URL="$OFFICER_REPO"

    if confirm "Clone it now?"; then
      if clone_repo "$CLONE_URL"; then
        ok "cloned to ${PLATFORM_DIR} on $(repo_branch)"
        SUMMARY+=("Repository: cloned from ${CLONE_URL}")
      else
        # GIT_TERMINAL_PROMPT=0 in clone_repo means this is a real failure rather
        # than a prompt nobody answered.
        fail "could not clone ${CLONE_URL} — nothing below can run without it."
      fi
    else
      fail "Nothing below can run without the repository."
    fi
  fi
  step_ok
fi

# =============================================================================
# 4. Dependencies
# =============================================================================

report_section "Dependencies"
step "Dependencies"
if ! skip; then
  echo ""
  info "Dependencies — bun install, as ${USERNAME}"
  echo "     node_modules: $(deps_installed && echo present || echo 'not there')"
  echo "     node-pty:     $(node_pty_built && echo built || echo 'not built')"
  echo ""
  echo "     The lockfile is frozen: bun resolves from bun.lock and nothing else,"
  echo "     so a package.json that disagrees with it fails rather than quietly"
  echo "     picking newer versions. That friction is deliberate."
  echo ""
  echo "     node-pty has no Linux prebuild, so this compiles it from source"
  echo "     every time — which is what build-essential and python3 are for."

  if deps_installed && node_pty_built; then
    ok "already installed, and node-pty is built"
    SUMMARY+=("Dependencies: already installed")
  elif confirm "Install them?"; then
    if install_deps; then
      if node_pty_built; then
        ok "installed, node-pty built"
        SUMMARY+=("Dependencies: installed")
      else
        # The install can succeed while the native module does not get built —
        # bun skips a dependency's lifecycle scripts unless it trusts the
        # package. Worth naming, because the symptom is a terminal that never
        # comes up rather than an install error.
        warn "installed, but node-pty has no built module at node_modules/node-pty/build/Release/"
        echo "     The terminal sidecar cannot start without it. Try:"
        echo "       cd $(platform_dir) && bun install --force"
        ERRORS+=("Dependencies: node-pty not built")
        SUMMARY+=("Dependencies: installed, node-pty NOT built")
      fi
    else
      warn "bun install failed"
      echo "     If it complained about the lockfile, package.json and bun.lock"
      echo "     disagree — that is the frozen lockfile doing its job, and it"
      echo "     wants a human to look at the diff."
      ERRORS+=("Dependencies: bun install failed")
      SUMMARY+=("Dependencies: FAILED")
    fi
  else
    warn "skipped by request"
    SUMMARY+=("Dependencies: SKIPPED by request")
  fi
  step_ok
fi

# =============================================================================
# 5. Database
# =============================================================================
#
# POSTGRES_URL is set here and written by the environment section below.

report_section "Database"
step "Database"
if ! skip; then
  echo ""
  info "Database — Postgres, the only one Officer has"
  echo "     It holds the account, passkeys, settings, dashboards, email"
  echo "     accounts and the job queue. Nothing else in the platform is a"
  echo "     database."
  echo ""
  # One network for everything Officer provisions. Created before the compose
  # file references it, since it is declared external there.
  if ensure_docker_network; then
    ok "docker network '${OFFICER_NETWORK}' created"
    SUMMARY+=("Docker network: ${OFFICER_NETWORK} created")
  elif docker_network_exists; then
    echo "     network:       ${OFFICER_NETWORK} (already there)"
  fi

  # The client goes on the HOST, before any of the container work, because it is the half
  # that is not in the container. A member has their own Postgres role and no access to the
  # owner's Docker socket, so `docker exec … psql` is the owner's tool, not theirs.
  install_pg_client || ERRORS+=("psql: client not installed — members have no Postgres CLI")

  echo "     compose file:  $(pg_compose_exists && echo "$(pg_compose_file)" || echo 'not written yet')"
  echo "     container:     $(pg_container_running && echo "${PG_CONTAINER} running" || echo 'not running')"
  echo "     port ${PG_PORT}:    $(pg_port_in_use && echo 'something is listening' || echo 'free')"

  POSTGRES_URL=""

  # An existing compose file means this ran before. Reuse its password rather
  # than minting a new one, which would leave the container and the URL
  # disagreeing about the credential.
  if pg_compose_exists && PG_EXISTING_PASSWORD="$(pg_password_from_env_file)"; then
    POSTGRES_URL="$(pg_url "$PG_EXISTING_PASSWORD")"
    echo ""
    echo "     already provisioned here — reusing the password from $(pg_env_file)"
    pg_container_running || {
      info "     starting it"
      pg_compose_up >/dev/null 2>&1 || true
    }
    if pg_wait_ready; then
      ok "postgres answering on 127.0.0.1:${PG_PORT}"
      SUMMARY+=("Database: existing Postgres at ${PG_CONTAINER}")
    else
      warn "the container is not answering — check: docker logs ${PG_CONTAINER}"
      ERRORS+=("Database: provisioned but not answering")
    fi

  else
    echo ""
    info "Which Postgres should Officer use?"
    echo ""
    echo "     [1] provision one here"
    echo "         ${PG_IMAGE} in $(pg_service_dir), bound to 127.0.0.1 only."
    echo "         Docker publishes ports by writing iptables rules beneath ufw,"
    echo "         so a database published to every interface is reachable from"
    echo "         the internet whatever the firewall says. Loopback is all the"
    echo "         platform needs — it runs on this machine."
    echo ""
    echo "     [2] use one you already run"
    echo "         Give the connection URL. Nothing is provisioned."
    echo ""

    DB_PICK=""
    while [[ -z "$DB_PICK" ]]; do
      if ! read -rp "  Which one? (1/2) [1]: " DB_CHOICE; then
        echo ""
        fail "No answer."
      fi
      case "${DB_CHOICE:-1}" in
        1)
          if ! command -v docker &>/dev/null; then
            warn "Docker is not installed, so there is nothing to provision into."
            continue
          fi
          if pg_port_in_use; then
            warn "something is already listening on ${PG_PORT} — provisioning here would fail to bind"
            echo "     If that is a Postgres you already run, pick 2 and give its URL."
            continue
          fi
          DB_PICK=provision
          ;;
        2) DB_PICK=existing ;;
        *) warn "Pick 1 or 2." ;;
      esac
    done

    if [[ "$DB_PICK" == provision ]]; then
      PG_PASSWORD="$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)"
      write_pg_compose "$PG_PASSWORD"
      ok "compose written to $(pg_compose_file)"
      if pg_compose_up && pg_wait_ready; then
        POSTGRES_URL="$(pg_url "$PG_PASSWORD")"
        ok "postgres answering on 127.0.0.1:${PG_PORT}, database '${PG_DATABASE}'"
        SUMMARY+=("Database: provisioned at $(pg_service_dir)")
      else
        warn "the container did not come up — check: docker logs ${PG_CONTAINER}"
        ERRORS+=("Database: container did not start")
        SUMMARY+=("Database: provisioning FAILED")
      fi
    else
      echo ""
      ask_required POSTGRES_URL "Connection URL" "postgresql://user:password@host:5432/officer"
      if pg_url_works "$POSTGRES_URL"; then
        ok "reachable"
        SUMMARY+=("Database: existing, ${POSTGRES_URL%%:*}://…")
      else
        # Not fatal. The URL may be right and the database not started yet, and
        # refusing to continue over that would be worse than saying so.
        warn "could not connect with that URL"
        echo "     Kept anyway — check it before running the schema step."
        ERRORS+=("Database: the given URL did not answer")
        SUMMARY+=("Database: existing URL kept, did not answer")
      fi
    fi
  fi
  step_ok
fi

# =============================================================================
# 6. Environment
# =============================================================================

report_section "Environment"
step "Environment"
if ! skip; then
  echo ""
  info "Environment — $(env_file)"

  # Read back before anything is asked; existing values become the defaults.
  ENV_PORT="$(env_get PORT)"
  ENV_PUBLIC_URL="$(env_get PUBLIC_URL)"

  if env_exists; then
    echo "     exists — its values are the defaults below"
  else
    echo "     does not exist yet"
  fi

  # ── what is asked ──
  echo ""
  ask_required ENV_PORT "Port Officer listens on" "${ENV_PORT:-9000}"

  echo ""
  echo "     PUBLIC_URL is where Officer is reached from a browser. It is the one"
  echo "     thing this machine cannot work out for itself, and three things need"
  echo "     it: the OpenGraph tags baked into the page by 'bun gen:index', the"
  echo "     host the task API hands to scripts, and the CalDAV profile an iPhone"
  echo "     installs — that last one requires https."
  echo ""
  echo "     Defaulting to this machine's tailnet address, not localhost: the"
  echo "     tailnet is where Officer is actually reached from, and localhost"
  echo "     works from here and nowhere else."
  ask_required ENV_PUBLIC_URL "Public URL" "${ENV_PUBLIC_URL:-$(default_public_url "$ENV_PORT")}"

  echo ""
  echo "     to write:"
  echo "       PORT=${ENV_PORT}"
  echo "       PUBLIC_URL=${ENV_PUBLIC_URL}"
  echo "       POSTGRES_URL=${POSTGRES_URL%%:*}://…"
  echo ""
  echo "     the install root is not written here — the platform derives it as the"
  echo "     parent of the repo, so data/, capabilities/ and dockers/ follow from"
  echo "     ${OFFICER_ROOT} without anything having to agree with anything."
  echo ""

  if confirm "Write it?"; then
    write_env
    ok "written, 0600, owned by ${USERNAME}"
    report_changed "wrote $(env_file) (0600, owner ${USERNAME}) — PORT, PUBLIC_URL, POSTGRES_URL. No secrets: every key lives in the secret store."
    [[ -f "$(env_file).before-officer-setup" ]] && echo "     previous kept as $(env_file).before-officer-setup"
    SUMMARY+=("Environment: $(env_file)")
  else
    warn "skipped by request"
    SUMMARY+=("Environment: SKIPPED by request")
  fi
  step_ok
fi

# =============================================================================
# 7. Secrets
# =============================================================================
#
# The store creates keys on demand, so this section is not strictly required —
# the first `sign()` would mint the jwt key by itself. It runs anyway for two
# reasons: the file should exist with the right owner and mode before anything
# races to create it, and an install that finishes without ever saying the words
# "back this up" is one where nobody learns the file matters until it is gone.

report_section "Secrets"
step "Secrets"
if ! skip; then
  echo ""
  info "Secret store — $(secret_store_path)"
  echo "     Every encryption and signing key the platform holds, one SQLite file,"
  echo "     one key per purpose. Nothing goes in .env."
  echo ""
  echo "     bootstrapped now:"
  echo "       jwt        signs every session token"
  echo "       headscale  encrypts the Headscale admin API key in Postgres"
  echo ""
  echo "     Every other purpose — wallet, photos, jellyfin, invoiceshelf, vault,"
  echo "     service-connections — is created when its plugin is installed. A"
  echo "     plugin cannot read another plugin's key."
  echo ""

  if confirm "Create it?"; then
    if bootstrap_secret_store; then
      ok "created, 0600, owned by ${USERNAME}"
      report_changed "created $(secret_store_path) (0600, dir 0700, owner ${USERNAME}) with keys for: jwt, headscale. Generated locally, never transmitted."
      echo ""
      warn "back up $(secret_store_path) — and keep it OUT of the backup that holds your database dump."
      echo "     Losing it signs everyone out and makes every encrypted column in"
      echo "     Postgres unreadable. For the wallet seed that is unrecoverable:"
      echo "     the passphrase opens the inner envelope, this is the outer one."
      echo ""
      echo "     Keeping it beside a dump defeats it — the dump is the ciphertext"
      echo "     and this is the key. Separate backups, or it is one theft."
      SUMMARY+=("Secrets: $(secret_store_path)")
    else
      warn "could not create the store — the platform will create it on first use"
      SUMMARY+=("Secrets: NOT created; the platform will do it on first use")
    fi
  else
    warn "skipped by request — the platform will create it on first use"
    SUMMARY+=("Secrets: SKIPPED; the platform will create it on first use")
  fi
  step_ok
fi

# =============================================================================
# 8. Schema
# =============================================================================

report_section "Schema"
step "Schema"
if ! skip; then
  echo ""
  # Counted from the aggregator rather than hardcoded, so the number is the truth
  # even when a plugin line is uncommented. It was written as ${SCHEMA_TABLES:-?}
  # and never assigned, so the section said "? tables" — a placeholder that looked
  # like the count could not be determined rather than like nobody had set it.
  SCHEMA_TABLES="$(schema_table_count)"
  info "Database schema"
  echo "     ${SCHEMA_TABLES:-?} tables, applied with 'bun db:push' — drizzle-kit"
  echo "     diffs the schema code against Postgres and alters it directly. There"
  echo "     are no migration files and no migration table; the code is the source"
  echo "     of truth."
  echo ""
  echo "     Only the CORE tables. Every plugin's tables are commented out in"
  echo "     src/databases/officer_db/src/schema.ts and get created when the"
  echo "     plugin is installed."
  echo ""

  if confirm "Push it?"; then
    if OUT="$(push_schema)"; then
      ok "schema applied"
      report_changed "applied ${SCHEMA_TABLES} tables to Postgres with 'bun db:push' (drizzle-kit; no migration files)"
      SUMMARY+=("Schema: ${SCHEMA_TABLES:-?} tables pushed")
    else
      warn "db:push failed"
      echo "$OUT" | tail -12 | sed 's/^/       /'
      SUMMARY+=("Schema: FAILED — see the output above")
    fi
  else
    warn "skipped by request — the platform will not start without it"
    SUMMARY+=("Schema: SKIPPED by request")
  fi
  step_ok
fi

# =============================================================================
# 9. Build
# =============================================================================

report_section "Build"
step "Build"
if ! skip; then
  echo ""
  info "index.gen.html"
  echo "     'bun gen:index' substitutes your public URL into index.html and"
  echo "     writes index.gen.html, which is the file the server imports. It is"
  echo "     gitignored, so a fresh clone never has one and the server has no page"
  echo "     to serve until this runs."
  echo ""
  echo "     URL: ${ENV_PUBLIC_URL:-<not set — run the Environment section first>}"
  echo ""
  echo "     To change it later: bun gen:index https://your.new.url"
  echo ""

  if [[ -z "$ENV_PUBLIC_URL" ]]; then
    warn "PUBLIC_URL is not in $(env_file) — run the Environment section, then this one"
    SUMMARY+=("Build: SKIPPED — no PUBLIC_URL")
  elif confirm "Generate it?"; then
    if OUT="$(gen_index)"; then
      ok "$(gen_index_output)"
      report_changed "generated $(gen_index_output) from index.html, substituting PUBLIC_URL=${ENV_PUBLIC_URL}"
      SUMMARY+=("Build: index.gen.html for ${ENV_PUBLIC_URL}")
    else
      warn "gen:index failed"
      echo "$OUT" | tail -8 | sed 's/^/       /'
      SUMMARY+=("Build: FAILED — see the output above")
    fi
  else
    warn "skipped by request — the server has no page to serve without it"
    SUMMARY+=("Build: SKIPPED by request")
  fi
  step_ok
fi

# =============================================================================
# 10. Services
# =============================================================================

report_section "Services"
step "Services"
if ! skip; then
  echo ""
  info "pm2 — $(ecosystem_file)"
  echo "     The ecosystem file is GENERATED, not checked in. It describes this"
  echo "     install and nothing else, so nothing in git can drift from it."
  echo ""
  echo "     six processes:"
  for entry in "${CORE_PROCESSES[@]}"; do
    IFS='|' read -r _name _script _args <<<"$entry"
    printf "       %-24s %s %s\n" "$_name" "$_script" "$_args"
  done
  echo ""
  echo "     Nothing else. Every plugin adds its own entry when it is installed."
  echo ""

  if confirm "Write it and start them?"; then
    write_ecosystem
    ok "written — $(ecosystem_file)"
    report_changed "wrote $(ecosystem_file) — six pm2 apps: $(printf '%s ' "${CORE_PROCESSES[@]%%|*}")"

    # Starting against a database that is not answering is not fatal — the server
    # waits and the agent retries forever — but it makes the Verify section below
    # report a failure that is really just a race, and that is the kind of noise
    # that teaches people to ignore a red line.
    if pg_container_running && ! pg_wait_ready 30; then
      warn "Postgres is not answering — starting anyway, but Verify may report failures"
    fi

    if OUT="$(pm2_start)"; then
      ok "processes started"
      report_started "pm2 startOrRestart: $(printf '%s ' "${CORE_PROCESSES[@]%%|*}")"
      pm2_save >/dev/null 2>&1 && ok "process list saved (survives a pm2 restart)"

      echo ""
      if confirm "Start them on boot too?"; then
        if pm2_enable_startup; then
          ok "pm2 will resurrect them at boot"
          report_ran "pm2 startup systemd — installed a systemd unit so pm2 resurrects these at boot"
          SUMMARY+=("Services: 6 processes started, enabled at boot")
        else
          warn "could not enable the boot hook — run 'pm2 startup' yourself and follow it"
          SUMMARY+=("Services: 6 processes started; boot hook NOT enabled")
        fi
      else
        SUMMARY+=("Services: 6 processes started; not enabled at boot")
      fi
    else
      warn "pm2 did not start cleanly"
      echo "$OUT" | tail -12 | sed 's/^/       /'
      SUMMARY+=("Services: FAILED to start — see the output above")
    fi
  else
    warn "skipped by request"
    SUMMARY+=("Services: SKIPPED by request")
  fi
  step_ok
fi

# =============================================================================
# 11. Verify
# =============================================================================

report_section "Verify"
step "Verify"
if ! skip; then
  echo ""
  info "Are the processes actually up?"
  echo ""

  VERIFY_BAD=0
  while IFS='|' read -r vname vstatus vrestarts; do
    [[ -z "$vname" ]] && continue
    if [[ "$vstatus" == "online" ]]; then
      if (( vrestarts > 3 )); then
        warn "$(printf '%-24s online, but restarted %s times — check: pm2 logs %s' "$vname" "$vrestarts" "$vname")"
        VERIFY_BAD=$((VERIFY_BAD + 1))
      else
        ok "$(printf '%-24s online' "$vname")"
      fi
    else
      warn "$(printf '%-24s %s — check: pm2 logs %s' "$vname" "$vstatus" "$vname")"
      VERIFY_BAD=$((VERIFY_BAD + 1))
    fi
  done < <(pm2_status_lines)

  echo ""
  # A process can be `online` and still be failing to serve — a restart loop takes
  # a few seconds to show up in the counter, and the app can be up with a broken
  # database. So the port is asked directly.
  if curl -fsS --max-time 5 "http://127.0.0.1:${ENV_PORT:-9000}/api" >/dev/null 2>&1; then
    ok "the API answers on 127.0.0.1:${ENV_PORT:-9000}"
    SUMMARY+=("Verify: API answering on port ${ENV_PORT:-9000}")
  else
    warn "nothing answered on 127.0.0.1:${ENV_PORT:-9000}/api"
    echo "     pm2 logs officer   is where the reason will be."
    VERIFY_BAD=$((VERIFY_BAD + 1))
    SUMMARY+=("Verify: the API did NOT answer on port ${ENV_PORT:-9000}")
  fi

  if (( VERIFY_BAD == 0 )); then
    echo ""
    ok "Officer is running. Open ${ENV_PUBLIC_URL:-http://localhost:${ENV_PORT:-9000}} and the"
    echo "     first-run screen will create the owner account."
  fi
  step_ok
fi


# =============================================================================
# 12. Proxy
# =============================================================================
#
# Optional, and last, because it is the only step that needs Officer to be already
# running: NPM proxies to it, and the gate below checks the bind address rather than
# taking a curl to loopback as proof.
#
# ── Why this section ignores --unattended ──
#
# Every other question in this script has a defensible default. None of these do — a
# domain name, a DNS provider and that provider's API credentials cannot be guessed —
# and the step is opt-in besides. So its prompts read stdin directly instead of going
# through confirm()/ask_required(), which honour ASSUME_YES.
#
# The valve is a TTY check, not the flag: with no terminal there is nobody to ask, so
# it skips and prints the manual instructions. That keeps a cron-driven install working
# without letting --unattended silently agree to publishing a public hostname.

report_section "Proxy"
step "Proxy"
if ! skip; then
  echo ""
  info "Reverse proxy — a real hostname and an HTTPS certificate"
  echo "     Optional. Skip it if you already run a proxy elsewhere, or if you"
  echo "     reach this instance over the tailnet and are happy with that."
  echo ""

  PROXY_PORT="${ENV_PORT:-9000}"

  if [[ ! -t 0 ]]; then
    warn "no terminal — skipping the proxy, which cannot be answered unattended"
    proxy_skip_instructions "$PROXY_PORT"
    SUMMARY+=("Proxy: skipped (no terminal)")
  elif ! proxy_require_listening "$PROXY_PORT"; then
    warn "skipping the proxy — Officer is not reachable the way NPM would reach it"
    echo "     Fix the bind address, then:  officer-setup.sh --only Proxy"
    SUMMARY+=("Proxy: skipped (Officer not listening on 0.0.0.0)")
  elif ! proxy_confirm "Set up Nginx Proxy Manager now?"; then
    proxy_skip_instructions "$PROXY_PORT"
    SUMMARY+=("Proxy: skipped by request")
  else
    # One failure path for all of it: every function warns and returns non-zero rather
    # than exiting, so a proxy that does not come up leaves a finished Officer install
    # behind rather than a failed one. It is the last section for that reason.
    PROXY_DOMAIN="$(proxy_ask 'Domain for this instance (e.g. officer.example.com)')"
    if [[ -z "$PROXY_DOMAIN" ]]; then
      warn "no domain given — skipping"
      SUMMARY+=("Proxy: skipped (no domain)")
    elif
      proxy_detect_target &&
        proxy_ensure_network &&
        proxy_order_docker_after_tailscaled &&
        proxy_write_compose &&
        proxy_start &&
        proxy_claim_admin &&
        proxy_get_token &&
        { [[ "$CHALLENGE" != "dns" ]] || proxy_prompt_dns_credentials; } &&
        proxy_wait_for_dns "$PROXY_DOMAIN" "$TARGET_IP" &&
        proxy_allow_bridge_to_host "$PROXY_PORT" &&
        proxy_create_host "$PROXY_DOMAIN" "$PROXY_PORT" &&
        proxy_issue_certificate "$PROXY_DOMAIN" &&
        proxy_attach_certificate
    then
      proxy_verify "$PROXY_DOMAIN"
      echo ""
      ok "Officer is published at https://${PROXY_DOMAIN}"
      echo "     NPM admin: http://127.0.0.1:81$([[ "$CHALLENGE" == "dns" ]] && echo " or http://${TARGET_IP}:81")"
      SUMMARY+=("Proxy: https://${PROXY_DOMAIN}")
    else
      warn "the proxy did not finish — Officer itself is unaffected and still running"
      echo "     Retry just this part with:  officer-setup.sh --only Proxy"
      ERRORS+=("Proxy: did not finish")
      SUMMARY+=("Proxy: FAILED — retry with --only Proxy")
    fi
  fi
  step_ok
fi

# ── Who you are when this exits ──
#
# Root, and that surprises people — reasonably, because everything this script just
# installed belongs to somebody else. The platform runs as ${USERNAME}: the checkout,
# node_modules, .env, the secret store and all six pm2 processes are theirs. Root was
# the installer's privilege, never the platform's.
#
# Saying so matters for two things that are invisible until they bite:
#
#   - group membership is fixed at login. ${USERNAME} was added to `docker` during
#     machine setup, and a session that started before that does not have it — so
#     `docker ps` fails for a reason that has nothing to do with docker.
#   - the shell config was written into THEIR home. Staying as root means none of it
#     is loaded, and the machine looks unconfigured.
if [[ "$EUID" -eq 0 ]]; then
  echo ""
  echo -e "${BOLD}  One more thing — you are still root.${NC}"
  echo ""
  echo "    Officer runs as ${USERNAME}, and everything it installed is theirs."
  echo "    Nothing here needs root any more. To carry on as them:"
  echo ""
  echo -e "      ${BOLD}su - ${USERNAME}${NC}                     from this session"
  echo -e "      ${BOLD}ssh ${USERNAME}@<this machine>${NC}       or log in fresh"
  echo ""
  echo "    Either gives a new session, which is what makes their docker group"
  echo "    membership and their shell configuration take effect. Staying as root"
  echo "    means neither does, and the machine will look half-configured."
fi

echo ""

report_mark_complete
