#!/usr/bin/env bash
#
# install-tak.sh - single-file installer for a local TAK Server 5.8 deployment.
#
# Ship this script next to takserver-docker-hardened-5.8-RELEASE-69.zip and
# nothing else. The compose file, CoreConfig template, certificate generator
# and default configuration are embedded below and written to ./.tak/ at run
# time; ./.tak/ is regenerated on every run, so edit this script, not its
# output. User-tunable settings live in ./.env, which is created on first run
# and never overwritten.
#
#   ./install-tak.sh                      certs -> config -> build -> up -> admin
#   ./install-tak.sh --no-build           skip image builds (config/cert changes only)
#   ./install-tak.sh --certs-only         stop after generating certificates
#   ./install-tak.sh --teardown [opts]    remove the deployment (see --help)
#   ./install-tak.sh --help               full documentation
#
# Safe to re-run. Certificates are generated once and reused from ./certs/files.
#
set -Eeuo pipefail

ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"

WORK="$ROOT/.tak"
CERT_STORE="$ROOT/certs/files"
STEP=0

log()  { printf '\n\033[1;36m==> [%d] %s\033[0m\n' "$((++STEP))" "$*"; }
info() { printf '    %s\n' "$*"; }
warn() { printf '\033[1;33m    WARNING: %s\033[0m\n' "$*" >&2; }
die()  { printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }

trap 'die "failed at line $LINENO: $BASH_COMMAND"' ERR

usage() {
  cat <<'USAGE_EOF'
install-tak.sh - local TAK Server 5.8 (hardened docker release)

A single-file installer. Put it in an empty directory next to
takserver-docker-hardened-5.8-RELEASE-69.zip and run it. The compose file,
CoreConfig template and certificate generator are embedded in the script and
written to ./.tak/ at run time.

QUICK START
    mkdir tak && cd tak
    cp /path/to/takserver-docker-hardened-5.8-RELEASE-69.zip .
    cp /path/to/install-tak.sh .
    ./install-tak.sh

    First run takes 15-30 minutes: two ~700 MB build contexts, base image
    pulls, a Postgres initdb at build time, then five JVMs starting. Later
    runs reuse the docker layer cache and take a few minutes.

USAGE
    ./install-tak.sh                    full install, safe to re-run
    ./install-tak.sh --no-build         skip image builds (config/cert changes only)
    ./install-tak.sh --certs-only       stop after generating certificates
    ./install-tak.sh --teardown         remove containers, keep the database volume
    ./install-tak.sh --teardown --volumes   also delete the database volume
    ./install-tak.sh --teardown --certs     also delete ./certs, forcing a new CA
    ./install-tak.sh --teardown --images    also remove the built images
    ./install-tak.sh --teardown --all       all of the above
    ./install-tak.sh --help             this text

REQUIREMENTS
    docker with the compose plugin, unzip, and ~15 GB of free disk.
    xmllint (apt-get install libxml2-utils) is optional; without it the
    CoreConfig schema check is skipped.

WHAT IT DOES
    1. Preflight    write ./.tak, create ./.env, extract the release zip,
                    normalise line endings, patch the health-check integrity
                    lists (see KNOWN VENDOR ISSUES)
    2. Config       render CoreConfig.xml from the embedded template using
                    .env, then validate it against the release's CoreConfig.xsd
    3. Certificates build the ca-setup image, generate a CA plus server, admin
                    and user certificates, optionally import admin.p12 into the
                    server keystore, export everything to ./certs/files
    4. Build        docker compose build
    5. Start        bring up the database, wait for the schema, start TAK Server
    6. Admin        register admin.pem with UserManager.jar
    7. Verify       check which certificate is served, that the admin alias is
                    where it should be, and that the admin console answers

PORTS
    All ports are mapped 1:1 to the container and are set in .env.

    8089   CoT streaming, TLS with client certificate
    8443   WebTAK and the non-admin UI. Admin console disabled here.
    8444   federation, validated against the federation truststore
    8446   certificate enrollment / OAuth, clientAuth=false
    8451   admin console only. WebTAK and non-admin UI disabled here.

    Browsing https://localhost:8443/ without a client certificate fails the
    TLS handshake and shows as a connection error, not an HTTP error page.
    That is the client-certificate requirement working, not a fault.

FILES
    install-tak.sh   this script - the only thing you distribute
    .env             created on first run, edit this to configure
    .tak/            generated on every run, do not edit
    certs/files/     the CA and all generated certificates
    takserver-docker-hardened-5.8-RELEASE-69/   extracted release

    ./certs/files/ca-do-not-share.key is the CA private key. Keep it out of
    version control and off shared drives.

CONFIGURATION
    Edit .env, then re-run. Changing ports or CoreConfig settings needs a
    rebuild because the config is baked into the images:
        ./install-tak.sh
    Changing certificate settings (CN, SANs, CA name) needs new certificates:
        ./install-tak.sh --teardown --certs && ./install-tak.sh

admin.p12 IN THE JKS
    IMPORT_ADMIN_P12_INTO_JKS=true imports the admin client certificate into
    the server keystore, <TAK_SERVER_CN>.jks. This is deliberate and was asked
    for, but it has a real consequence worth understanding.

    CoreConfig sets keymanager="SunX509". SunX509 does not filter key entries
    by extended key usage - faced with two RSA entries it picks by alias order,
    so the server can end up presenting the admin client certificate to
    browsers instead of its own server certificate. Step 7 checks for exactly
    this and warns if it happens.

    If you hit it, set IMPORT_ADMIN_P12_INTO_JKS=false in .env and run
    --teardown --certs followed by a normal run. Switching keymanager to
    NewSunX509, which does filter by EKU, is the other option.

KNOWN VENDOR ISSUES WORKED AROUND
    CoreConfig.example.xml does not validate against its own CoreConfig.xsd.
    The shipped example still uses the pre-5.x <qos enabled="...">
    <reportingRateLimiter> spelling. In the 5.8 schema qos takes no attributes
    and its children are deliveryRateLimiter / readRateLimiter / dosRateLimiter.
    The embedded template uses the 5.8 spelling.

    critical_file_list.txt names files that no longer exist. Both images run
    check_file_integrity.sh as part of their HEALTHCHECK, which sha256-sums
    every path in that list and fails if any one is unreadable. The 5.8 lists
    name /usr/share/crypto-policies/back-ends/*/openssl.config, which current
    crypto-policies replaced with opensslcnf.config. Left alone this pins both
    containers at (unhealthy) forever while Postgres and the API are fine, and
    the only evidence is inside /opt/tak/health/*/health_check.log in the
    container. Step 1 comments those paths out; step 4 then re-checks every
    remaining path inside the built images so the next such drift is reported
    by name in seconds rather than as an unexplained unhealthy container.

    COPY --exclude needs a labs frontend. Dockerfile.hardened-takserver-db
    uses `COPY --exclude=`, a labs-only flag, with no `# syntax=` directive of
    its own, so whether it builds depends on the host's default Dockerfile
    frontend - it succeeds on some machines and fails on others with
    "unknown flag: --exclude", after initdb has already run. Step 1 pins
    `# syntax=docker/dockerfile:1.7-labs` on any Dockerfile that needs it.

TROUBLESHOOTING
    Container stuck (unhealthy)
        docker exec <container> tail /opt/tak/health/<name>/health_check.log
    TAK Server will not start
        docker compose -f .tak/docker-compose.yml logs takserver
    Database connection refused
        The password in .env must be alphanumeric; it is written into
        CoreConfig.xml as an XML attribute and parsed back out with sed.
    "another TAK Server install is already deployed from ..."
        Container names are global to the docker daemon, so only one
        deployment can exist per host regardless of directory. Preflight
        checks the compose working-directory label on each container and
        stops before the build rather than colliding on the first `up`.
        Tear the other one down, or remove the named containers by hand.
    Start completely over
        ./install-tak.sh --teardown --all
        docker builder prune -af      # optional, forces a genuinely cold build
        ./install-tak.sh

NOT CONFIGURED HERE
    Federation with other servers, LDAP or OAuth authentication, plugins,
    clustering, and TLS certificates from a public CA. This is a single-host
    local deployment with a self-signed CA.
USAGE_EOF
}

DO_BUILD=1
CERTS_ONLY=0
TEARDOWN=0
DROP_VOLUMES=0
DROP_CERTS=0
DROP_IMAGES=0
for arg in "$@"; do
  case "$arg" in
    --no-build)   DO_BUILD=0 ;;
    --certs-only) CERTS_ONLY=1 ;;
    --teardown)   TEARDOWN=1 ;;
    --volumes)    DROP_VOLUMES=1 ;;
    --certs)      DROP_CERTS=1 ;;
    --images)     DROP_IMAGES=1 ;;
    --all)        DROP_VOLUMES=1; DROP_CERTS=1; DROP_IMAGES=1 ;;
    -h|--help)    usage; exit 0 ;;
    *)            die "unknown argument: $arg (try --help)" ;;
  esac
done

# --volumes/--certs/--images only mean anything alongside --teardown; silently
# ignoring a typo here would delete nothing and look like success.
if [ "$TEARDOWN" = 0 ] && { [ "$DROP_VOLUMES" = 1 ] || [ "$DROP_CERTS" = 1 ] || [ "$DROP_IMAGES" = 1 ]; }; then
  die "--volumes/--certs/--images require --teardown"
fi

# ---------------------------------------------------------------------------
# Embedded assets
#
# Written on every run so the script stays the single source of truth. Each
# heredoc is quoted, so $VAR and ${VAR} inside them reach disk untouched -
# docker compose and the cert generator expand their own variables later.
# ---------------------------------------------------------------------------
write_assets() {
  mkdir -p "$WORK"

  cat > "$WORK/docker-compose.yml" <<'COMPOSE_EOF'
# TAK Server 5.8 (hardened docker release), local single-host deployment.
#
# Run ./install-tak.sh rather than `docker compose up` directly - the images bake
# in the generated CoreConfig.xml and certificates, so those have to exist in
# the build context first.

networks:
  takserver-net:
    name: ${TAK_NETWORK}
    driver: bridge
    ipam:
      config:
        - subnet: ${TAK_SUBNET}

volumes:
  tak-db-data:

services:
  tak-database:
    build:
      context: ${TAK_SRC}
      dockerfile: docker/Dockerfile.hardened-takserver-db
    image: tak-database-hardened:${TAK_VERSION}
    container_name: ${DB_CONTAINER}
    hostname: ${DB_CONTAINER}
    networks:
      - takserver-net
    volumes:
      # Postgres is initdb'd during the image build; the first start copies
      # that into the named volume so data survives image rebuilds.
      - tak-db-data:/var/lib/postgresql/data
    restart: unless-stopped

  takserver:
    build:
      context: ${TAK_SRC}
      dockerfile: docker/Dockerfile.hardened-takserver
    image: takserver-hardened:${TAK_VERSION}
    container_name: ${TAKSERVER_CONTAINER}
    hostname: ${TAKSERVER_CONTAINER}
    depends_on:
      tak-database:
        condition: service_healthy
    environment:
      # The image's HEALTHCHECK curls the API with this client cert. Without
      # both variables the container never reports healthy.
      ADMIN_CERT_NAME: ${ADMIN_CERT_NAME}
      ADMIN_CERT_PASS: ${CAPASS}
    networks:
      - takserver-net
    ports:
      - "${PORT_COT_TLS}:${PORT_COT_TLS}"       # CoT streaming, TLS client cert
      - "${PORT_WEBTAK}:${PORT_WEBTAK}"         # WebTAK + non-admin UI
      - "${PORT_FEDERATION}:${PORT_FEDERATION}" # federation
      - "${PORT_ENROLLMENT}:${PORT_ENROLLMENT}" # cert enrollment / OAuth
      - "${PORT_ADMIN}:${PORT_ADMIN}"           # admin console
    restart: unless-stopped
COMPOSE_EOF

  cat > "$WORK/CoreConfig.xml.tmpl" <<'CORECONFIG_EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!--
  Rendered by install-tak.sh into <TAK_SRC>/tak/CoreConfig.xml, which is baked
  into both the takserver and tak-database images at build time.

  Do not edit the generated CoreConfig.xml by hand for values that come from
  .env - it is overwritten on every run. Changes made through the
  Admin UI live only inside the running container.
-->
<Configuration xmlns="http://bbn.com/marti/xml/config"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="CoreConfig.xsd">
    <network tomcatMaxPool="300" esyncEnableCache="1" multicastTTL="5">
        <input _name="stdssl" protocol="tls" port="@PORT_COT_TLS@" coreVersion="2"/>

        <!-- Web connectors.

             enableWebtak / enableAdminUI / enableNonAdminUI let each HTTPS
             connector expose a different subset of the web tier
             (TAK Server Configuration Guide, section 9.9). Defaults are all
             true, so anything not disabled here is reachable.

             @PORT_WEBTAK@  - WebTAK + non-admin UI. Admin console disabled.
             @PORT_ADMIN@  - Admin console only. WebTAK and non-admin UI disabled.
        -->
        <connector port="@PORT_WEBTAK@" _name="https"
                   enableWebtak="true"
                   enableNonAdminUI="true"
                   enableAdminUI="false"/>

        <connector port="@PORT_ADMIN@" _name="admin_https"
                   enableAdminUI="true"
                   enableWebtak="false"
                   enableNonAdminUI="false"/>

        <!-- Federation connector, validated against the federation truststore. -->
        <connector port="@PORT_FEDERATION@" _name="fed_https"
                   useFederationTruststore="true"
                   enableWebtak="false"
                   enableAdminUI="false"/>

        <!-- Certificate enrollment / OAuth connector. clientAuth="false" so a
             client with no cert yet can reach the enrollment endpoint. Nothing
             administrative is exposed here. -->
        <connector port="@PORT_ENROLLMENT@" _name="cert_https"
                   clientAuth="false"
                   enableWebtak="false"
                   enableAdminUI="false"/>
    </network>

    <auth>
        <File location="UserAuthenticationFile.xml"/>
    </auth>

    <submission ignoreStaleMessages="false" validateXml="false"/>

    <subscription reloadPersistent="false"/>

    <repository enable="true" numDbConnections="16" primaryKeyBatchSize="500" insertionBatchSize="500">
        <connection url="jdbc:postgresql://@DB_CONTAINER@:5432/@DB_NAME@" username="@DB_USER@" password="@DB_PASSWORD@"/>
    </repository>

    <repeater enable="true" periodMillis="3000" staleDelayMillis="15000">
        <repeatableType initiate-test="/event/detail/emergency[@type='911 Alert']" cancel-test="/event/detail/emergency[@cancel='true']" _name="911"/>
        <repeatableType initiate-test="/event/detail/emergency[@type='Ring The Bell']" cancel-test="/event/detail/emergency[@cancel='true']" _name="RingTheBell"/>
        <repeatableType initiate-test="/event/detail/emergency[@type='Geo-fence Breached']" cancel-test="/event/detail/emergency[@cancel='true']" _name="GeoFenceBreach"/>
        <repeatableType initiate-test="/event/detail/emergency[@type='Troops In Contact']" cancel-test="/event/detail/emergency[@cancel='true']" _name="TroopsInContact"/>
    </repeater>

    <dissemination smartRetry="false"/>

    <filter>
        <flowtag enable="false" text=""/>
        <streamingbroker enable="true"/>
        <scrubber enable="false" action="overwrite"/>
        <!-- 5.8 XSD: qos takes no attributes; the rate limiter elements carry
             their own enabled flag. The shipped CoreConfig.example.xml still
             uses the pre-5.x <qos enabled=""><reportingRateLimiter> spelling,
             which does not validate against the CoreConfig.xsd next to it. -->
        <qos>
            <deliveryRateLimiter enabled="false">
                <rateLimitRule clientThresholdCount="1000" reportingRateLimitSeconds="120"/>
                <rateLimitRule clientThresholdCount="5000" reportingRateLimitSeconds="180"/>
                <rateLimitRule clientThresholdCount="10000" reportingRateLimitSeconds="240"/>
                <rateLimitRule clientThresholdCount="25000" reportingRateLimitSeconds="480"/>
                <rateLimitRule clientThresholdCount="50000" reportingRateLimitSeconds="960"/>
            </deliveryRateLimiter>
        </qos>
    </filter>

    <buffer>
        <latestSA enable="true"/>
        <queue/>
    </buffer>

    <security>
        <tls context="TLSv1.2"
             keymanager="SunX509"
             keystore="JKS" keystoreFile="/opt/tak/certs/files/@TAK_SERVER_CN@.jks" keystorePass="@CAPASS@"
             truststore="JKS" truststoreFile="/opt/tak/certs/files/truststore-root.jks" truststorePass="@CAPASS@"/>
    </security>
</Configuration>
CORECONFIG_EOF

  cat > "$WORK/gen-tak-certs.sh" <<'GENCERTS_EOF'
#!/bin/bash
#
# Entrypoint override for the ca-setup image.
#
# Stands in for the shipped generateClusterCertsIfNoneExist.sh so that the
# server certificate can be issued under an arbitrary Common Name (the shipped
# generateClusterCerts.sh hardcodes "takserver") and can carry extra SANs.
#
# Everything it calls - cert-metadata.sh, makeRootCa.sh, makeCert.sh - is the
# vendor's own, unmodified. Required environment:
#
#   CA_NAME, STATE, CITY, ORGANIZATIONAL_UNIT   consumed by cert-metadata.sh
#   TAK_SERVER_CN                               server cert CN, keystore name
#   ADMIN_CERT_NAME                             admin client cert CN
#   EXTRA_SANS                                  optional, openssl alt_names lines
#   CAPASS / PASS                               keystore passwords
#
set -euo pipefail

cd /tak/certs

CERT_DIR=/tak/certs/files

if [ -d "$CERT_DIR" ] && [ -n "$(ls -A "$CERT_DIR" 2>/dev/null)" ]; then
  echo "Existing certificates found at $CERT_DIR. New certificates will not be generated."
else
  : "${CA_NAME:?CA_NAME must be set}"
  : "${TAK_SERVER_CN:?TAK_SERVER_CN must be set}"
  : "${ADMIN_CERT_NAME:?ADMIN_CERT_NAME must be set}"

  # makeCert.sh emits exactly one alt name for a server cert: it copies
  # config.cfg, then appends "subjectAltName = @alt_names" followed by an
  # [alt_names] section holding DNS.1 (or IP.1) = <CN>.
  #
  # Extra SANs are added by appending our own [alt_names] entries here.
  # OpenSSL's config parser merges repeated section headers, so our block and
  # the one makeCert.sh appends become a single [alt_names] section.
  #
  # The trailing empty "[ server ]" header matters: config.cfg must still end
  # inside the server section, or makeCert.sh's "subjectAltName = @alt_names"
  # would land inside [alt_names] instead of [server] and the SAN extension
  # would never be requested.
  if [ -n "${EXTRA_SANS:-}" ] && ! grep -q '^# local_tak extra SANs' config.cfg; then
    {
      printf '\n# local_tak extra SANs\n[ alt_names ]\n'
      printf '%b\n' "$EXTRA_SANS"
      printf '\n[ server ]\n'
    } >> config.cfg
  fi

  echo "Generating TAK Server certificates..."
  ./cert-metadata.sh
  ./makeRootCa.sh --ca-name "$CA_NAME"
  ./makeCert.sh server "$TAK_SERVER_CN"
  ./makeCert.sh client user
  ./makeCert.sh client "$ADMIN_CERT_NAME"
  echo "Certificate generation complete."
fi

# Hold the container open so install-tak.sh can exec keytool and docker cp the
# results out.
sleep infinity
GENCERTS_EOF
  chmod +x "$WORK/gen-tak-certs.sh"

  cat > "$WORK/env.defaults" <<'ENVDEF_EOF'
# ---------------------------------------------------------------------------
# local_tak configuration
#
# install-tak.sh writes this to .env on first run and generates a random DB
# password. Edit .env, not this block - this is the embedded default.
#
# This file is sourced by bash and read by docker compose, so quote any value
# containing spaces and do not put spaces around '='.
#
# Changing certificate or port settings after the first run requires
#   ./install-tak.sh --teardown --certs && ./install-tak.sh
# ---------------------------------------------------------------------------

# --- release / build context ------------------------------------------------
TAK_SRC=./takserver-docker-hardened-5.8-RELEASE-69
TAK_VERSION=5.8-RELEASE-69

# --- docker naming ----------------------------------------------------------
COMPOSE_PROJECT_NAME=local-tak
TAK_NETWORK=takserver-net
TAK_SUBNET=172.28.0.0/16
TAKSERVER_CONTAINER=takserver
DB_CONTAINER=tak-database
CA_CONTAINER=tak-ca-setup

# --- certificate authority --------------------------------------------------
# CA_NAME must be at least 5 characters, contain no spaces, and differ from
# TAK_SERVER_CN (makeRootCa.sh and makeCert.sh both enforce this).
CA_NAME=local-tak-ca
CERT_COUNTRY=US
CERT_STATE="Virginia"
CERT_CITY="Vienna"
CERT_ORGANIZATION="TAK"
CERT_ORGANIZATIONAL_UNIT="local"

# Password for every generated keystore, truststore and .p12. atakatak is TAK's
# default; the browser asks for this when importing admin.p12.
CAPASS=atakatak

# Common Name of the server certificate - the hostname you browse to.
TAK_SERVER_CN=localhost

# Extra openssl alt_names entries appended to the server certificate, so the
# same cert also works by container name and over 127.0.0.1. Keys must not
# collide with DNS.1, which makeCert.sh sets to TAK_SERVER_CN. Use \n between
# entries; gen-tak-certs.sh expands them.
TAK_SERVER_EXTRA_SANS="DNS.2 = takserver\nDNS.3 = takserver.local\nIP.1 = 127.0.0.1"

# Client certificate used for the admin account.
ADMIN_CERT_NAME=admin

# Import admin.p12 into the server keystore (<TAK_SERVER_CN>.jks) inside the
# takserver container. Read "admin.p12 in the JKS" in ./install-tak.sh --help
# before changing this.
IMPORT_ADMIN_P12_INTO_JKS=true

# --- database ---------------------------------------------------------------
DB_NAME=cot
DB_USER=martiuser
# Generated on first run. Alphanumeric only: it is written into
# CoreConfig.xml as an XML attribute and parsed back out with sed by the
# database container's setup script.
DB_PASSWORD=

# --- published ports --------------------------------------------------------
# Mapped 1:1 to the container, and substituted into CoreConfig.xml, so these
# are both the host port and the port TAK Server listens on.
PORT_COT_TLS=8089
PORT_WEBTAK=8443
PORT_FEDERATION=8444
PORT_ENROLLMENT=8446
PORT_ADMIN=8451
ENVDEF_EOF

  cat > "$WORK/README" <<'WORKREADME_EOF'
Generated by install-tak.sh on every run. Nothing here is authoritative -
edit install-tak.sh (or ./.env) instead; changes made in this directory are
overwritten the next time the installer runs.
WORKREADME_EOF
}

# ---------------------------------------------------------------------------
# 1. Preflight
# ---------------------------------------------------------------------------
log "Preflight"

command -v docker >/dev/null 2>&1 || die "docker not found on PATH"
docker info >/dev/null 2>&1 || die "cannot talk to the docker daemon (is it running, and is your user in the docker group?)"

write_assets
info "wrote embedded assets to ./.tak"

if [ ! -f .env ]; then
  info "no .env found, creating one from the embedded defaults"
  cp "$WORK/env.defaults" .env
  # Alphanumeric only: this value is substituted into CoreConfig.xml as an XML
  # attribute and parsed back out by the database container with sed.
  generated="$(od -An -tx1 -N24 /dev/urandom | tr -d ' \n' | cut -c1-32)"
  sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=${generated}|" .env
  info "generated a random DB_PASSWORD in .env"
fi

set -a
# shellcheck disable=SC1091
source ./.env
set +a

: "${TAK_SRC:?TAK_SRC not set in .env}"
: "${TAK_SERVER_CN:?TAK_SERVER_CN not set in .env}"
: "${DB_PASSWORD:?DB_PASSWORD is empty in .env}"

if docker compose version >/dev/null 2>&1; then
  COMPOSE=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
  COMPOSE=(docker-compose)
else
  die "neither 'docker compose' nor 'docker-compose' is available"
fi
# The compose file lives in .tak/, but its build contexts and .env are relative
# to the project root, so both have to be named explicitly.
COMPOSE+=(--project-directory "$ROOT" --env-file "$ROOT/.env" -f "$WORK/docker-compose.yml")
info "compose: docker compose -f .tak/docker-compose.yml"

SRC="$ROOT/${TAK_SRC#./}"

# ---------------------------------------------------------------------------
# Refuse to fight another install for the container names.
#
# docker-compose.yml pins container_name, and container names are global to the
# docker daemon rather than scoped per compose project. A second install
# anywhere on this host therefore gets six steps in and dies on the first `up`
# with a raw "Conflict. The container name ... is already in use" from the
# daemon, which names a container ID and says nothing about the cause.
#
# Compose stamps every container it creates with the directory it was started
# from, so that label tells us who owns one. Checked before the teardown block
# on purpose: tearing down from the wrong directory would remove someone else's
# deployment just as happily as `up` would collide with it.
# ---------------------------------------------------------------------------
check_container_owner() {
  local name="$1" owner
  docker container inspect "$name" >/dev/null 2>&1 || return 0
  owner="$(docker container inspect "$name" \
    --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' 2>/dev/null || true)"
  if [ -z "$owner" ]; then
    die "a container named '$name' already exists and was not created by this installer.
    Inspect it, and remove it if it is not something you need:
        docker inspect $name
        docker rm -f $name"
  fi
  if [ "$owner" != "$ROOT" ]; then
    die "another TAK Server install is already deployed from
        $owner
    Container names are global to the docker daemon, so only one deployment can
    exist per host - and both would want ports ${PORT_WEBTAK}, ${PORT_ADMIN} and ${PORT_COT_TLS} anyway.
    Tear that one down first:
        (cd $owner && ./install-tak.sh --teardown --all)"
  fi
}
for _c in "$DB_CONTAINER" "$TAKSERVER_CONTAINER"; do check_container_owner "$_c"; done

# ---------------------------------------------------------------------------
# Teardown, then exit. Placed after .env is loaded because every name it
# removes comes from there.
# ---------------------------------------------------------------------------
if [ "$TEARDOWN" = 1 ]; then
  log "Teardown"
  if [ "$DROP_VOLUMES" = 1 ]; then
    printf '\033[1;33m    This deletes the TAK Server database volume and every mission, user and file in it.\033[0m\n'
    read -r -p "    Type 'yes' to continue: " confirm
    [ "$confirm" = "yes" ] || die "aborted"
    info "stopping containers and removing volumes"
    "${COMPOSE[@]}" down --volumes --remove-orphans
  else
    info "stopping containers (database volume preserved)"
    "${COMPOSE[@]}" down --remove-orphans
  fi

  info "removing the ca-setup container"
  docker rm -f "$CA_CONTAINER" >/dev/null 2>&1 || true

  if [ "$DROP_CERTS" = 1 ]; then
    info "deleting ./certs and the staged certificates"
    rm -rf "$ROOT/certs" "$SRC/tak/certs/files" "$SRC/tak/certs/gen-tak-certs.sh"
    docker rmi -f "tak-ca-setup:${TAK_VERSION}" >/dev/null 2>&1 || true
    info "the next run will generate a new CA"
  fi

  if [ "$DROP_IMAGES" = 1 ]; then
    info "removing built images"
    for img in "takserver-hardened" "tak-database-hardened" "tak-ca-setup"; do
      docker rmi -f "${img}:${TAK_VERSION}" >/dev/null 2>&1 || true
    done
  fi

  rm -rf "$WORK"
  printf '\n\033[1;32m%s\033[0m\n' 'Teardown complete.'
  exit 0
fi

if [ ! -d "$SRC" ]; then
  zip_file="$ROOT/${TAK_SRC#./}.zip"
  [ -f "$zip_file" ] || die "neither $SRC nor $zip_file exists - put the release zip next to this script"
  command -v unzip >/dev/null 2>&1 || die "unzip not found on PATH (apt-get install unzip)"
  info "extracting $(basename "$zip_file") (this takes a minute)"
  unzip -q -o "$zip_file" -d "$ROOT"
fi
[ -f "$SRC/docker/Dockerfile.hardened-takserver" ] || die "$SRC does not look like the hardened docker release"
info "source: $SRC"

[ "$CA_NAME" != "$TAK_SERVER_CN" ] || die "CA_NAME and TAK_SERVER_CN must differ (makeCert.sh refuses otherwise)"
[ "${#CA_NAME}" -ge 5 ] || die "CA_NAME must be at least 5 characters, or makeRootCa.sh substitutes a random name"

# The release ships world-writable files; docker build fails on some hosts
# unless the tak tree is readable/executable by the build user.
chmod -R u+rwX "$SRC/tak"

# Guard against CRLF sneaking in via a Windows-side copy (see the vendor
# README's WSL note) - a CR in a shebang breaks every script in the image.
crlf_files="$(grep -rlU --include='*.sh' -e $'\r' "$SRC/tak" 2>/dev/null || true)"
if [ -n "$crlf_files" ]; then
  warn "CRLF line endings found under $SRC/tak; converting to LF"
  printf '%s\n' "$crlf_files" | xargs -r sed -i 's/\r$//'
fi

# Both images' HEALTHCHECKs end in check_file_integrity.sh, which sha256-sums
# every path in critical_file_list.txt and fails the whole check if any single
# one is unreadable. The 5.8 lists still name crypto-policies back-end files
# that current Rocky no longer ships - openssl.config was superseded by
# opensslcnf.config - so without this the containers sit at (unhealthy)
# forever while postgres and the API are both perfectly fine. The failure is
# silent unless you read /opt/tak/health/*/health_check.log inside the
# container, and it costs 10 retries x 30s before docker gives up.
#
# The script skips lines whose first non-space character is '#', so commenting
# is enough. Idempotent: a second run finds nothing left to change.
STALE_INTEGRITY_PATHS=(
  /usr/share/crypto-policies/back-ends/LEGACY/openssl.config
  /usr/share/crypto-policies/back-ends/FUTURE/openssl.config
  /usr/share/crypto-policies/back-ends/DEFAULT/openssl.config
  /usr/share/crypto-policies/back-ends/FIPS/openssl.config
)
for list_file in "$SRC"/tak/health/*/critical_file_list.txt; do
  [ -f "$list_file" ] || continue
  for stale in "${STALE_INTEGRITY_PATHS[@]}"; do
    if grep -qxF "$stale" "$list_file"; then
      sed -i "s|^$(printf '%s' "$stale" | sed 's/[|.]/\\&/g')$|# &  # not shipped by current crypto-policies|" "$list_file"
      info "integrity list ($(basename "$(dirname "$list_file")")): commented out $stale"
    fi
  done
done

# Dockerfile.hardened-takserver-db uses `COPY --exclude=`, which is a labs-only
# flag, and the vendor file carries no `# syntax=` directive. Whether it builds
# therefore depends on which Dockerfile frontend the host's BuildKit happens to
# default to: recent ones accept the flag, older ones stop with
#   dockerfile parse error on line 252: unknown flag: --exclude
# after the build has already run initdb. Pinning the labs frontend makes the
# build independent of the host instead of a coin toss.
for dockerfile in "$SRC"/docker/Dockerfile.*; do
  [ -f "$dockerfile" ] || continue
  grep -qE '^[[:space:]]*(COPY|ADD)[[:space:]].*--exclude' "$dockerfile" || continue
  head -1 "$dockerfile" | grep -q '^# syntax=' && continue
  sed -i '1i # syntax=docker/dockerfile:1.7-labs' "$dockerfile"
  info "pinned the labs Dockerfile frontend in $(basename "$dockerfile") (COPY --exclude needs it)"
done

# ---------------------------------------------------------------------------
# 2. Render CoreConfig.xml into the build context
# ---------------------------------------------------------------------------
log "Rendering CoreConfig.xml"

CORE_CONFIG="$SRC/tak/CoreConfig.xml"
sed \
  -e "s|@PORT_COT_TLS@|${PORT_COT_TLS}|g" \
  -e "s|@PORT_WEBTAK@|${PORT_WEBTAK}|g" \
  -e "s|@PORT_ADMIN@|${PORT_ADMIN}|g" \
  -e "s|@PORT_FEDERATION@|${PORT_FEDERATION}|g" \
  -e "s|@PORT_ENROLLMENT@|${PORT_ENROLLMENT}|g" \
  -e "s|@DB_CONTAINER@|${DB_CONTAINER}|g" \
  -e "s|@DB_NAME@|${DB_NAME}|g" \
  -e "s|@DB_USER@|${DB_USER}|g" \
  -e "s|@DB_PASSWORD@|${DB_PASSWORD}|g" \
  -e "s|@TAK_SERVER_CN@|${TAK_SERVER_CN}|g" \
  -e "s|@CAPASS@|${CAPASS}|g" \
  "$WORK/CoreConfig.xml.tmpl" > "$CORE_CONFIG"

if grep -qE '@[A-Z_][A-Z0-9_]*@' "$CORE_CONFIG"; then
  grep -nE '@[A-Z_][A-Z0-9_]*@' "$CORE_CONFIG" >&2
  die "unsubstituted placeholders remain in CoreConfig.xml"
fi

# Optional, but catches a bad edit to the template before a 20 minute build.
# Install with: sudo apt-get install libxml2-utils
if command -v xmllint >/dev/null 2>&1; then
  # Captured rather than piped: under `set -o pipefail`, `grep -q` can close the
  # pipe early and surface xmllint's SIGPIPE as a validation failure.
  xmllint_out="$(xmllint --noout --schema "$SRC/tak/CoreConfig.xsd" "$CORE_CONFIG" 2>&1)" || true
  if printf '%s' "$xmllint_out" | grep -q 'validates'; then
    info "CoreConfig.xml validates against CoreConfig.xsd"
  else
    printf '%s\n' "$xmllint_out" >&2
    die "CoreConfig.xml does not validate against CoreConfig.xsd"
  fi
else
  info "xmllint not installed, skipping schema validation"
fi

info "WebTAK   -> port ${PORT_WEBTAK} (enableWebtak=true, enableAdminUI=false)"
info "Admin UI -> port ${PORT_ADMIN} (enableAdminUI=true, enableWebtak=false)"
info "database -> ${DB_USER}@${DB_CONTAINER}:5432/${DB_NAME}"

# ---------------------------------------------------------------------------
# 3. Certificates
# ---------------------------------------------------------------------------
log "Certificates"

if [ -f "$CERT_STORE/ca.pem" ]; then
  info "reusing the existing CA and certificates in ./certs/files"
  info "(run ./install-tak.sh --teardown --certs to regenerate from scratch)"
else
  info "no CA found, generating a fresh one"

  # Dockerfile.ca bakes tak/certs/ into the image, and the generator skips work
  # when files/ is non-empty. Clear it so generation actually runs.
  rm -rf "$SRC/tak/certs/files"
  cp "$WORK/gen-tak-certs.sh" "$SRC/tak/certs/gen-tak-certs.sh"
  chmod +x "$SRC/tak/certs/gen-tak-certs.sh"

  info "building the ca-setup image"
  docker build -q -t "tak-ca-setup:${TAK_VERSION}" \
    --build-arg ARG_CA_NAME="$CA_NAME" \
    --build-arg ARG_STATE="$CERT_STATE" \
    --build-arg ARG_CITY="$CERT_CITY" \
    --build-arg ARG_ORGANIZATIONAL_UNIT="$CERT_ORGANIZATIONAL_UNIT" \
    -f "$SRC/docker/Dockerfile.ca" "$SRC" >/dev/null

  # Keep it out of the takserver image; it is only needed for the CA build.
  rm -f "$SRC/tak/certs/gen-tak-certs.sh"

  docker rm -f "$CA_CONTAINER" >/dev/null 2>&1 || true

  info "generating the CA plus server/admin/user certificates"
  docker run -d --name "$CA_CONTAINER" \
    --entrypoint /tak/certs/gen-tak-certs.sh \
    -e CA_NAME="$CA_NAME" \
    -e COUNTRY="$CERT_COUNTRY" \
    -e STATE="$CERT_STATE" \
    -e CITY="$CERT_CITY" \
    -e ORGANIZATION="$CERT_ORGANIZATION" \
    -e ORGANIZATIONAL_UNIT="$CERT_ORGANIZATIONAL_UNIT" \
    -e CAPASS="$CAPASS" \
    -e PASS="$CAPASS" \
    -e TAK_SERVER_CN="$TAK_SERVER_CN" \
    -e ADMIN_CERT_NAME="$ADMIN_CERT_NAME" \
    -e EXTRA_SANS="$TAK_SERVER_EXTRA_SANS" \
    "tak-ca-setup:${TAK_VERSION}" >/dev/null

  # admin.p12 is the last artifact the generator produces.
  ok=0
  for _ in $(seq 1 150); do
    if [ -z "$(docker ps -q --filter "name=^${CA_CONTAINER}$")" ]; then
      docker logs "$CA_CONTAINER" >&2 || true
      die "$CA_CONTAINER exited before finishing certificate generation"
    fi
    if docker exec "$CA_CONTAINER" bash -c "test -f /tak/certs/files/${ADMIN_CERT_NAME}.p12" 2>/dev/null; then
      ok=1
      break
    fi
    sleep 2
  done
  if [ "$ok" != 1 ]; then
    docker logs "$CA_CONTAINER" >&2 || true
    die "timed out waiting for certificate generation"
  fi
  info "certificates generated"

  # -------------------------------------------------------------------------
  # Import admin.p12 into the server keystore.
  #
  # Done here, inside the CA container, because that is the only place holding
  # both stores before they are exported. See "admin.p12 in the JKS" in --help
  # for why this can affect certificate selection.
  # -------------------------------------------------------------------------
  if [ "${IMPORT_ADMIN_P12_INTO_JKS,,}" = "true" ]; then
    info "importing ${ADMIN_CERT_NAME}.p12 into ${TAK_SERVER_CN}.jks"
    docker exec \
      -e KEYSTORE="${TAK_SERVER_CN}.jks" \
      -e ALIAS="$ADMIN_CERT_NAME" \
      -e STOREPASS="$CAPASS" \
      "$CA_CONTAINER" bash -c '
        set -euo pipefail
        cd /tak/certs/files
        if keytool -list -keystore "$KEYSTORE" -storepass "$STOREPASS" -alias "$ALIAS" >/dev/null 2>&1; then
          echo "    alias \"$ALIAS\" is already present in $KEYSTORE, skipping"
          exit 0
        fi
        keytool -importkeystore -noprompt \
          -srckeystore  "${ALIAS}.p12" -srcstoretype PKCS12 \
          -srcstorepass "$STOREPASS"   -srcalias "$ALIAS" \
          -destkeystore "$KEYSTORE"    -deststoretype JKS \
          -deststorepass "$STOREPASS"  -destkeypass "$STOREPASS" \
          -destalias "$ALIAS"
        echo "    $KEYSTORE now contains:"
        keytool -list -keystore "$KEYSTORE" -storepass "$STOREPASS" 2>/dev/null \
          | { grep -iE "entry," || true; } | sed "s/^/      /"
      '
  else
    info "IMPORT_ADMIN_P12_INTO_JKS is not true, leaving ${TAK_SERVER_CN}.jks untouched"
  fi

  info "exporting certificates to ./certs/files"
  mkdir -p "$ROOT/certs"
  rm -rf "$CERT_STORE"
  docker cp "$CA_CONTAINER:/tak/certs/files" "$ROOT/certs" >/dev/null
  docker stop "$CA_CONTAINER" >/dev/null
  info "the CA private key is in ./certs/files/ca-do-not-share.key - keep it out of version control"
fi

# Copy only what the server needs into the build context. Deliberately not the
# whole directory: ca-do-not-share.key must not end up inside the image.
log "Staging certificates into the build context"
mkdir -p "$SRC/tak/certs/files"
for f in \
  "${TAK_SERVER_CN}.jks" \
  "truststore-root.jks" \
  "truststore-root.p12" \
  "fed-truststore.jks" \
  "${ADMIN_CERT_NAME}.pem" \
  "${ADMIN_CERT_NAME}.p12" \
  "user.p12" \
  "ca.pem" \
  "config-${TAK_SERVER_CN}.cfg"
do
  [ -f "$CERT_STORE/$f" ] || die "expected certificate file missing: certs/files/$f"
  cp "$CERT_STORE/$f" "$SRC/tak/certs/files/"
done
chmod -R u+rwX "$SRC/tak/certs/files"
info "staged $(find "$SRC/tak/certs/files" -maxdepth 1 -type f | wc -l) files"

if [ "$CERTS_ONLY" = 1 ]; then
  log "Done (--certs-only)"
  exit 0
fi

# ---------------------------------------------------------------------------
# 4. Build
# ---------------------------------------------------------------------------
if [ "$DO_BUILD" = 1 ]; then
  log "Building images (the first run pulls base images and takes a while)"
  "${COMPOSE[@]}" build
else
  log "Skipping the image build (--no-build)"
fi

# The block in step 1 fixes the drift we know about. This catches the next one:
# it resolves each built image's integrity list inside that image, so a base
# image that drops another file is reported here - by name, in seconds - rather
# than as an unexplained (unhealthy) five minutes into step 5.
check_integrity_list() {
  local image="$1" health_dir="$2" out
  docker image inspect "$image" >/dev/null 2>&1 || { info "$image not built, skipping integrity list check"; return 0; }
  out="$(docker run --rm --entrypoint bash "$image" -c '
    list=/opt/tak/health/'"$health_dir"'/critical_file_list.txt
    [ -r "$list" ] || exit 0
    while read -r line; do
      f=$(echo "$line" | tr -d " ")
      [ -z "$f" ] && continue
      case "$f" in \#*) continue;; esac
      [ -r "$f" ] || echo "$f"
    done < "$list"
  ' 2>/dev/null)"
  if [ -n "$out" ]; then
    printf '%s\n' "$out" | sed 's/^/      /' >&2
    warn "$image: the paths above are in critical_file_list.txt but missing from the image;"
    warn "its HEALTHCHECK can never pass. Add them to STALE_INTEGRITY_PATHS in this script and rebuild."
    return 1
  fi
  info "$image: integrity list resolves"
}

log "Checking health-check integrity lists against the built images"
integrity_ok=1
check_integrity_list "tak-database-hardened:${TAK_VERSION}" takserver-db || integrity_ok=0
check_integrity_list "takserver-hardened:${TAK_VERSION}"    takserver    || integrity_ok=0
[ "$integrity_ok" = 1 ] || die "health checks would never pass; fix the integrity lists above and re-run"

# ---------------------------------------------------------------------------
# 5. Start the database, wait for the schema, then start TAK Server
# ---------------------------------------------------------------------------
log "Starting the database"
"${COMPOSE[@]}" up -d tak-database

info "waiting for SchemaManager to create the ${DB_NAME} schema"
db_ready=0
for i in $(seq 1 120); do
  tables="$(docker exec "$DB_CONTAINER" psql -U postgres -d "$DB_NAME" -tAc \
    "SELECT count(*) FROM information_schema.tables WHERE table_schema='public'" 2>/dev/null || true)"
  tables="${tables//[[:space:]]/}"
  if [ -n "$tables" ] && [ "$tables" -gt 0 ] 2>/dev/null; then
    info "schema ready after ~$((i * 5))s (${tables} tables)"
    db_ready=1
    break
  fi
  sleep 5
done
[ "$db_ready" = 1 ] || warn "database schema not confirmed; TAK Server will retry its connection on its own"

log "Starting TAK Server"
"${COMPOSE[@]}" up -d takserver

# ---------------------------------------------------------------------------
# 6. Wait for the API, then register the admin certificate
# ---------------------------------------------------------------------------
log "Waiting for the TAK Server API to accept connections"
# The PORT_WEBTAK connector sets no clientAuth attribute, so Tomcat defaults to
# requiring a client certificate there - only the enrollment connector sets
# clientAuth="false". Curling it without one fails the TLS handshake and yields
# 000 forever, so this wait could never succeed no matter how long it ran. The
# image's own HEALTHCHECK passes admin.p12 for exactly this reason; match it.
api_up=0
code=""
API_TRIES=240
for i in $(seq 1 $API_TRIES); do
  if [ -z "$(docker ps -q --filter "name=^${TAKSERVER_CONTAINER}$")" ]; then
    die "$TAKSERVER_CONTAINER is not running; check 'docker compose -f .tak/docker-compose.yml logs takserver'"
  fi
  code="$(docker exec "$TAKSERVER_CONTAINER" curl -sk -o /dev/null -w '%{http_code}' \
            --max-time 5 \
            --cert-type P12 --cert "/opt/tak/certs/files/${ADMIN_CERT_NAME}.p12:${CAPASS}" \
            "https://localhost:${PORT_WEBTAK}/Marti/api/version" 2>/dev/null || true)"
  if [ -n "$code" ] && [ "$code" != "000" ]; then
    info "API responding after ~$((i * 5))s (HTTP $code)"
    api_up=1
    break
  fi
  # A cold first boot against an empty database volume runs the full schema
  # migration before Tomcat binds. Report progress every minute rather than
  # sitting silent for twenty.
  if [ $((i % 12)) -eq 0 ]; then
    info "still waiting, ~$((i * 5))s elapsed (docker health: $(docker inspect "$TAKSERVER_CONTAINER" --format '{{.State.Health.Status}}' 2>/dev/null || echo unknown))"
  fi
  sleep 5
done
if [ "$api_up" != 1 ]; then
  warn "last HTTP code was '${code:-none}'; 000 means the TLS handshake failed, not that the server is down"
  warn "last lines of the container's own health check log:"
  docker exec "$TAKSERVER_CONTAINER" tail -10 /opt/tak/health/takserver/health_check.log 2>/dev/null | sed 's/^/      /' >&2 || true
  warn "last 30 lines of the takserver log:"
  "${COMPOSE[@]}" logs --tail 30 takserver 2>/dev/null | sed 's/^/      /' >&2 || true
  die "timed out waiting for the API on port ${PORT_WEBTAK} after $((API_TRIES * 5))s"
fi

log "Registering ${ADMIN_CERT_NAME}.pem as a TAK Server administrator"
registered=0
for attempt in $(seq 1 6); do
  if docker exec -w /opt/tak "$TAKSERVER_CONTAINER" \
       java -jar /opt/tak/utils/UserManager.jar certmod -A "/opt/tak/certs/files/${ADMIN_CERT_NAME}.pem"
  then
    info "admin registered"
    registered=1
    break
  fi
  info "attempt ${attempt}/6 failed, retrying in 15s"
  sleep 15
done
[ "$registered" = 1 ] || die "could not register the admin certificate with UserManager.jar"

# ---------------------------------------------------------------------------
# 7. Verify
# ---------------------------------------------------------------------------
log "Verifying"

# Which certificate does the server actually present? With two RSA key entries
# in the keystore, SunX509 picks by alias order rather than by extended key
# usage, so this is worth checking explicitly.
served_subject="$(docker exec "$TAKSERVER_CONTAINER" \
  keytool -printcert -sslserver "localhost:${PORT_WEBTAK}" 2>/dev/null \
  | awk -F'Owner: ' '/^Owner:/ {print $2; exit}' || true)"

if [ -n "$served_subject" ]; then
  info "port ${PORT_WEBTAK} presents: $served_subject"
  if printf '%s' "$served_subject" | grep -qE "CN=${ADMIN_CERT_NAME}(,|$)"; then
    warn "the server is presenting the ADMIN client certificate, not the server certificate."
    warn "SunX509 selected the wrong key entry from ${TAK_SERVER_CN}.jks."
    warn "Fix: set IMPORT_ADMIN_P12_INTO_JKS=false in .env, then run"
    warn "  ./install-tak.sh --teardown --certs && ./install-tak.sh"
    warn "See 'admin.p12 in the JKS' in ./install-tak.sh --help."
  fi
else
  warn "could not read the certificate served on port ${PORT_WEBTAK}"
fi

if docker exec "$TAKSERVER_CONTAINER" keytool -list \
     -keystore "/opt/tak/certs/files/${TAK_SERVER_CN}.jks" \
     -storepass "$CAPASS" -alias "$ADMIN_CERT_NAME" >/dev/null 2>&1
then
  info "alias '${ADMIN_CERT_NAME}' is present in /opt/tak/certs/files/${TAK_SERVER_CN}.jks"
elif [ "${IMPORT_ADMIN_P12_INTO_JKS,,}" = "true" ]; then
  warn "alias '${ADMIN_CERT_NAME}' is NOT in ${TAK_SERVER_CN}.jks inside the container"
  warn "(if you enabled the import after certificates already existed, run --teardown --certs first)"
else
  info "alias '${ADMIN_CERT_NAME}' is absent from ${TAK_SERVER_CN}.jks, as configured"
fi

admin_code="$(docker exec "$TAKSERVER_CONTAINER" curl -sk -o /dev/null -w '%{http_code}' --max-time 10 \
  --cert-type P12 --cert "/opt/tak/certs/files/${ADMIN_CERT_NAME}.p12:${CAPASS}" \
  "https://localhost:${PORT_ADMIN}/Marti/api/version" 2>/dev/null || true)"
if [ -n "$admin_code" ] && [ "$admin_code" != "000" ]; then
  info "admin console port ${PORT_ADMIN} answered HTTP ${admin_code} to the admin certificate"
else
  warn "nothing answered on port ${PORT_ADMIN} yet; it may still be starting"
fi

# ---------------------------------------------------------------------------
printf '\n\033[1;32m%s\033[0m\n' 'TAK Server is up.'
cat <<EOF

  WebTAK        https://${TAK_SERVER_CN}:${PORT_WEBTAK}
  Admin console https://${TAK_SERVER_CN}:${PORT_ADMIN}
  CoT streaming ${TAK_SERVER_CN}:${PORT_COT_TLS}  (TLS, client certificate)

  Import ./certs/files/${ADMIN_CERT_NAME}.p12 into your browser to reach the
  admin console. The password is CAPASS from .env (currently: ${CAPASS}).

  Logs      docker compose -f .tak/docker-compose.yml logs -f takserver
  Teardown  ./install-tak.sh --teardown --all

EOF
