#!/usr/bin/env bash
set -euo pipefail
set +x
set +a
umask 077
IFS=$' \t\n'

# Do not inherit alternate curl trust stores or per-user curl configuration at
# this supply-chain boundary. Proxy variables remain available for networks
# that require an outbound proxy, but TLS still terminates against system roots.
unset CURL_CA_BUNDLE SSL_CERT_FILE SSL_CERT_DIR SSLKEYLOGFILE
unset CURL_HOME NETRC CURL_SSL_BACKEND

# LabTether release installer.
# Usage: curl -fsSL https://labtether.com/install.sh | bash
# Pin a release explicitly with: LABTETHER_VERSION=v1.7.0 bash install.sh

REPOSITORY="labtether/labtether"
DIR="${LABTETHER_INSTALL_DIR:-labtether}"
VERSION="${LABTETHER_VERSION:-}"
REQUIRE_ATTESTATION="${LABTETHER_REQUIRE_ATTESTATION:-false}"
SKIP_ATTESTATION="${LABTETHER_SKIP_ATTESTATION:-false}"

for boolean_value in "${REQUIRE_ATTESTATION}" "${SKIP_ATTESTATION}"; do
  if [[ "${boolean_value}" != "true" && "${boolean_value}" != "false" ]]; then
    echo "  ✗ Attestation controls must be exactly true or false."
    exit 1
  fi
done
if [[ "${REQUIRE_ATTESTATION}" == "true" && "${SKIP_ATTESTATION}" == "true" ]]; then
  echo "  ✗ Attestation verification cannot be both required and skipped."
  exit 1
fi

echo ""
echo "  ┌─────────────────────────────────┐"
echo "  │  LabTether — Verified Install   │"
echo "  └─────────────────────────────────┘"
echo ""

for cmd in awk chmod curl dirname docker install mkdir mktemp mv rm rmdir sort stat tr wc; do
  if ! command -v "${cmd}" >/dev/null 2>&1; then
    echo "  ✗ ${cmd} is required but not installed."
    exit 1
  fi
done
if [[ "$(printf '%s' "${DIR}" | LC_ALL=C tr -d '[:cntrl:]')" != "${DIR}" ]]; then
  echo "  ✗ Install path must not contain control characters."
  exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
  echo "  ✗ docker compose (v2) is required but not available."
  exit 1
fi

secure_curl() {
  # -q must be curl's first option to suppress ~/.curlrc before it can weaken
  # TLS or redirect/output policy.
  curl -q --fail --silent --show-error --location \
    --retry 3 --connect-timeout 15 --max-time 300 \
    --proto '=https' --proto-redir '=https' --tlsv1.2 "$@"
}

validate_compose_release() {
  local env_file="$1" compose_file="$2"
  local services="" images="" service_count="" image_count="" image=""
  local expected_labtether="ghcr.io/${REPOSITORY}:${VERSION}"
  local found_expected=false

  if ! docker compose --env-file "${env_file}" -f "${compose_file}" config --quiet; then
    echo "  ✗ Release compose configuration is invalid."
    return 1
  fi
  if ! services="$(docker compose --env-file "${env_file}" -f "${compose_file}" config --services)" ||
     ! images="$(docker compose --env-file "${env_file}" -f "${compose_file}" config --images)"; then
    echo "  ✗ Could not inspect release compose services and images."
    return 1
  fi
  service_count="$(printf '%s\n' "${services}" | awk 'NF { count++ } END { print count + 0 }')"
  image_count="$(printf '%s\n' "${images}" | awk 'NF { count++ } END { print count + 0 }')"
  if [[ "${service_count}" -eq 0 || "${service_count}" -ne "${image_count}" ]]; then
    echo "  ✗ Every release service must resolve to exactly one image."
    return 1
  fi
  while IFS= read -r image; do
    [[ -n "${image}" ]] || continue
    if [[ ! "${image}" =~ ^[A-Za-z0-9._/:@-]+@sha256:[a-f0-9]{64}$ ]]; then
      echo "  ✗ Every release image must use an immutable sha256 digest."
      return 1
    fi
    if [[ "${image%%@sha256:*}" == "${expected_labtether}" ]]; then
      found_expected=true
    fi
  done <<< "${images}"
  if [[ "${found_expected}" != "true" ]]; then
    echo "  ✗ Release compose does not contain the expected LabTether tag and digest."
    return 1
  fi
}
compose_version="$(docker compose version --short)"
if [[ ! "${compose_version}" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+) ]] ||
   (( BASH_REMATCH[1] < 2 )) ||
   (( BASH_REMATCH[1] == 2 && BASH_REMATCH[2] < 33 )) ||
   (( BASH_REMATCH[1] == 2 && BASH_REMATCH[2] == 33 && BASH_REMATCH[3] < 1 )); then
  echo "  ✗ Docker Compose 2.33.1 or newer is required (found ${compose_version:-unknown})."
  exit 1
fi

if [[ -z "${VERSION}" ]]; then
  latest_url="$(secure_curl --head -o /dev/null -w '%{url_effective}' "https://github.com/${REPOSITORY}/releases/latest")"
  VERSION="${latest_url##*/}"
fi
if [[ ${#VERSION} -gt 128 || ! "${VERSION}" =~ ^v[0-9][0-9A-Za-z._-]*$ ]]; then
  echo "  ✗ Could not resolve a valid release version. Set LABTETHER_VERSION to a release tag, for example v1.7.0."
  exit 1
fi

tmp_dir="$(mktemp -d)"
TEMP_INSTALL_PATHS=()
INSTALL_LOCK=""
cleanup() {
  local temporary=""
  if [[ ${#TEMP_INSTALL_PATHS[@]} -gt 0 ]]; then
    for temporary in "${TEMP_INSTALL_PATHS[@]}"; do
      [[ -z "${temporary}" ]] || rm -f "${temporary}" >/dev/null 2>&1 || true
    done
  fi
  if [[ -n "${INSTALL_LOCK}" ]]; then
    rmdir "${INSTALL_LOCK}" >/dev/null 2>&1 || true
  fi
  rm -rf "${tmp_dir}"
}
trap cleanup EXIT
if [[ -L "${tmp_dir}" || ! -d "${tmp_dir}" || ! -O "${tmp_dir}" ]]; then
  echo "  ✗ Could not create a private installer workspace."
  exit 1
fi
chmod 700 "${tmp_dir}"

release_base="https://github.com/${REPOSITORY}/releases/download/${VERSION}"
# GitHub renames uploaded assets that start with a dot, so the release ships the
# environment template as deploy.env.example; it is installed as .env.deploy.
artifacts=(docker-compose.deploy.yml deploy.env.example deploy-manifest.json SHA256SUMS)
echo "  → Downloading release inputs for ${VERSION}…"
for artifact in "${artifacts[@]}"; do
  case "${artifact}" in
    docker-compose.deploy.yml) artifact_limit=5242880 ;;
    deploy.env.example|deploy-manifest.json|SHA256SUMS) artifact_limit=1048576 ;;
    *) echo "  ✗ Internal installer artifact allowlist error."; exit 1 ;;
  esac
  secure_curl --max-filesize "${artifact_limit}" \
    "${release_base}/${artifact}" -o "${tmp_dir}/${artifact}"
  if [[ -L "${tmp_dir}/${artifact}" || ! -f "${tmp_dir}/${artifact}" ]]; then
    echo "  ✗ Downloaded release input is not a regular file: ${artifact}"
    exit 1
  fi
  artifact_size="$(wc -c < "${tmp_dir}/${artifact}" | tr -d '[:space:]')"
  if [[ ! "${artifact_size}" =~ ^[0-9]+$ || "${artifact_size}" -gt "${artifact_limit}" ]]; then
    echo "  ✗ Downloaded release input exceeds its size limit: ${artifact}"
    exit 1
  fi
done

# A checksum manifest is only useful when it names exactly the files this
# installer downloaded. Reject absolute paths, traversal, duplicates, and
# extra entries before asking the platform checksum tool to open anything.
expected_checksum_names=$'deploy-manifest.json\ndeploy.env.example\ndocker-compose.deploy.yml'
checksum_names="$(awk '
  NF != 2 || length($1) != 64 || $1 ~ /[^0-9a-f]/ { exit 2 }
  {
    name = $2
    sub(/^\*/, "", name)
    if (name !~ /^(docker-compose\.deploy\.yml|deploy\.env\.example|deploy-manifest\.json)$/) { exit 3 }
    if (seen[name]++) { exit 4 }
    print name
  }
' "${tmp_dir}/SHA256SUMS" | LC_ALL=C sort)" || {
  echo "  ✗ Release checksum manifest contains unsafe or unexpected entries."
  exit 1
}
if [[ "${checksum_names}" != "${expected_checksum_names}" ]]; then
  echo "  ✗ Release checksum manifest is incomplete."
  exit 1
fi

(
  cd "${tmp_dir}"
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum --check SHA256SUMS
  else
    shasum -a 256 --check SHA256SUMS
  fi
)
echo "  ✓ Release checksums verified"

# When GitHub CLI is present, also bind each downloaded artifact to the
# repository's tag-triggered release workflow and GitHub-hosted runner. This
# is independent of the checksum file carried beside the release artifacts.
if [[ "${SKIP_ATTESTATION}" == "true" ]]; then
  echo "  ! Artifact provenance verification explicitly skipped."
elif command -v gh >/dev/null 2>&1; then
  echo "  → Verifying signed release provenance…"
  for artifact in "${artifacts[@]}"; do
    if ! gh attestation verify "${tmp_dir}/${artifact}" \
      --hostname github.com \
      --repo "${REPOSITORY}" \
      --signer-workflow "${REPOSITORY}/.github/workflows/release.yml" \
      --source-ref "refs/tags/${VERSION}" \
      --deny-self-hosted-runners >/dev/null; then
      echo "  ✗ Signed provenance verification failed for ${artifact}."
      exit 1
    fi
  done
  echo "  ✓ Signed release provenance verified"
elif [[ "${REQUIRE_ATTESTATION}" == "true" ]]; then
  echo "  ✗ GitHub CLI is required by LABTETHER_REQUIRE_ATTESTATION=true."
  exit 1
else
  echo "  ! GitHub CLI not found; continuing with release checksums and digest-pinned images."
  echo "    Install gh and set LABTETHER_REQUIRE_ATTESTATION=true to require signed provenance."
fi

validate_compose_release "${tmp_dir}/deploy.env.example" \
  "${tmp_dir}/docker-compose.deploy.yml"

if [[ -e "${DIR}" && ! -d "${DIR}" ]]; then
  echo "  ✗ Install path exists and is not a directory: ${DIR}"
  exit 1
fi
if [[ -L "${DIR}" ]]; then
  echo "  ✗ Refusing to install through a symlinked install directory: ${DIR}"
  exit 1
fi
mkdir -p -- "${DIR}"
if [[ ! -O "${DIR}" ]]; then
  echo "  ✗ Refusing an install directory not owned by the current user: ${DIR}"
  exit 1
fi
DIR="$(cd -P -- "${DIR}" && pwd)"
if [[ "${DIR}" == "/" ]]; then
  echo "  ✗ Refusing to use the filesystem root as the install directory."
  exit 1
fi
chmod go-w "${DIR}"

# The parent controls whether another local user can replace the entire install
# directory between validation and the atomic renames below. A sticky parent
# (for example /tmp) is safe because the install directory is owned by us;
# otherwise reject group/world-writable parents.
install_parent="$(dirname -- "${DIR}")"
if stat -c '%a' "${install_parent}" >/dev/null 2>&1; then
  install_parent_mode="$(stat -c '%a' "${install_parent}")"
else
  install_parent_mode="$(stat -f '%Lp' "${install_parent}")"
fi
if (( (8#${install_parent_mode} & 0022) != 0 )) && [[ ! -k "${install_parent}" ]]; then
  echo "  ✗ Refusing a group/world-writable non-sticky install parent: ${install_parent}"
  exit 1
fi

lock_path="${DIR}/.labtether-install.lock"
if ! mkdir "${lock_path}"; then
  echo "  ✗ Another install is running or a stale install lock exists: ${lock_path}"
  exit 1
fi
chmod 700 "${lock_path}"
INSTALL_LOCK="${lock_path}"

for destination in docker-compose.deploy.yml deploy-manifest.json SHA256SUMS .env.deploy; do
  if [[ -L "${DIR}/${destination}" ]]; then
    echo "  ✗ Refusing a symlinked install target: ${DIR}/${destination}"
    exit 1
  fi
  if [[ -e "${DIR}/${destination}" && ! -f "${DIR}/${destination}" ]]; then
    echo "  ✗ Existing install target must be a regular file: ${DIR}/${destination}"
    exit 1
  fi
done
if [[ -e "${DIR}/.env.deploy" && ! -O "${DIR}/.env.deploy" ]]; then
  echo "  ✗ Existing deploy environment must be a regular file owned by the current user."
  exit 1
fi
if [[ -e "${DIR}/.env.deploy" ]]; then
  if stat -c '%h' "${DIR}/.env.deploy" >/dev/null 2>&1; then
    env_link_count="$(stat -c '%h' "${DIR}/.env.deploy")"
  else
    env_link_count="$(stat -f '%l' "${DIR}/.env.deploy")"
  fi
  if [[ "${env_link_count}" -ne 1 ]]; then
    echo "  ✗ Existing deploy environment must not be hard-linked."
    exit 1
  fi
  env_size="$(wc -c < "${DIR}/.env.deploy" | tr -d '[:space:]')"
  if [[ ! "${env_size}" =~ ^[0-9]+$ || "${env_size}" -gt 1048576 ]]; then
    echo "  ✗ Existing deploy environment exceeds the 1 MiB safety limit."
    exit 1
  fi
  validate_compose_release "${DIR}/.env.deploy" "${tmp_dir}/docker-compose.deploy.yml"
fi

install_atomically() {
  local source=$1 destination=$2 mode=$3 temporary
  temporary="$(mktemp "${DIR}/.${destination}.new.XXXXXX")"
  TEMP_INSTALL_PATHS+=("${temporary}")
  install -m "${mode}" "${source}" "${temporary}"
  mv -f "${temporary}" "${DIR}/${destination}"
  if [[ -L "${DIR}/${destination}" || ! -f "${DIR}/${destination}" || ! -O "${DIR}/${destination}" ]]; then
    echo "  ✗ Atomic installation did not produce a safe regular file."
    exit 1
  fi
}

install_atomically "${tmp_dir}/deploy-manifest.json" deploy-manifest.json 0644
install_atomically "${tmp_dir}/SHA256SUMS" SHA256SUMS 0644
install_atomically "${tmp_dir}/docker-compose.deploy.yml" docker-compose.deploy.yml 0644
if [[ ! -f "${DIR}/.env.deploy" ]]; then
  install_atomically "${tmp_dir}/deploy.env.example" .env.deploy 0600
  echo "  ✓ Created ${DIR}/.env.deploy"
else
  chmod 600 "${DIR}/.env.deploy"
  echo "  ✓ Preserved existing ${DIR}/.env.deploy"
fi

# Revalidate the exact installed compose file with the preserved environment.
# This catches unsafe or syntactically invalid local overrides before `up`.
validate_compose_release "${DIR}/.env.deploy" "${DIR}/docker-compose.deploy.yml"

# Copied from hub scripts/lib/postgres-volume.sh; keep the two in step.
# v1.4.0 mounted postgres-data at /var/lib/postgresql/data, but PostgreSQL 18
# keeps its cluster in /var/lib/postgresql/18/docker, so those databases live in
# the postgres container's unnamed volume. Copy that cluster into postgres-data
# once, before the new compose file starts PostgreSQL, and leave the unnamed
# volume untouched as a fallback. If the old container was already replaced,
# name the old volume in LABTETHER_POSTGRES_LEGACY_VOLUME.
labtether_move_legacy_postgres_volume() {
  local container project legacy_volume named_volume require_cluster=0
  legacy_volume="${LABTETHER_POSTGRES_LEGACY_VOLUME:-}"
  if [[ -n "${legacy_volume}" ]]; then
    require_cluster=1
    if ! docker volume inspect "${legacy_volume}" >/dev/null 2>&1; then
      echo "  ✗ LABTETHER_POSTGRES_LEGACY_VOLUME names a volume that does not exist: ${legacy_volume}"
      return 1
    fi
  else
    container="$(docker compose "$@" ps --all --quiet postgres)" || return 1
    if [[ -z "${container}" ]]; then
      return 0
    fi
    if [[ "${container}" == *$'\n'* ]]; then
      echo "  ✗ Found more than one postgres container; remove the extra one before upgrading."
      return 1
    fi
    named_volume="$(docker inspect --format '{{range .Mounts}}{{if and (eq .Type "volume") (eq .Destination "/var/lib/postgresql/data")}}{{.Name}}{{end}}{{end}}' "${container}")" || return 1
    if [[ -z "${named_volume}" ]]; then
      # postgres-data is not mounted at the old path: already the PostgreSQL 18 layout.
      return 0
    fi
    project="$(docker inspect --format '{{index .Config.Labels "com.docker.compose.project"}}' "${container}")" || return 1
    legacy_volume="$(docker inspect --format '{{range .Mounts}}{{if and (eq .Type "volume") (eq .Destination "/var/lib/postgresql")}}{{.Name}}{{end}}{{end}}' "${container}")" || return 1
    if [[ -z "${project}" || "${named_volume}" != "${project}_postgres-data" || -z "${legacy_volume}" || "${legacy_volume}" == "${named_volume}" ]]; then
      echo "  ✗ Postgres container ${container} has an unexpected volume layout; not moving its data."
      return 1
    fi
  fi

  echo "  → Moving the PostgreSQL database from volume ${legacy_volume} into postgres-data…"
  docker compose "$@" stop || return 1
  # The one-off container uses the new postgres service definition, so
  # /var/lib/postgresql is exactly the postgres-data volume the stack will use.
  # An empty old volume means PostgreSQL never stored data there; only the
  # marker is written and the new stack starts with an empty database.
  docker compose "$@" run --rm --no-deps --no-TTY --env "LABTETHER_REQUIRE_CLUSTER=${require_cluster}" \
    --volume "${legacy_volume}:/labtether-legacy-postgres:ro" --entrypoint /bin/sh postgres -ec '
      from=/labtether-legacy-postgres
      to=/var/lib/postgresql
      for existing in "$to"/PG_VERSION "$to"/*/docker/PG_VERSION; do
        if [ -e "$existing" ]; then
          echo "postgres-data already holds a database ($existing); not overwriting it" >&2
          exit 1
        fi
      done
      if [ -s "$from/18/docker/PG_VERSION" ]; then
        cp -a "$from/." "$to/"
        test -s "$to/18/docker/PG_VERSION"
      elif [ "$LABTETHER_REQUIRE_CLUSTER" = 1 ]; then
        echo "the named volume holds no PostgreSQL 18 database" >&2
        exit 1
      fi
      : > "$to/.labtether-postgres-volume"
    ' || {
    echo "  ✗ Could not move the database; volume ${legacy_volume} is unchanged."
    return 1
  }
  echo "  ✓ Moved the database into postgres-data; volume ${legacy_volume} is kept as a backup."
}

(
  cd "${DIR}"
  labtether_move_legacy_postgres_volume --env-file .env.deploy -f docker-compose.deploy.yml
)

echo "  → Starting LabTether ${VERSION}…"
(
  cd "${DIR}"
  docker compose --env-file .env.deploy -f docker-compose.deploy.yml up -d
)

printf -v quoted_dir '%q' "${DIR}"

echo ""
echo "  ✓ LabTether ${VERSION} is running from checksum-verified, digest-pinned artifacts."
echo "  Open http://localhost:3000 to get started."
echo "  Retrieve the one-time owner setup token locally when you are ready to use it:"
echo "    cd -- ${quoted_dir} && docker compose --env-file .env.deploy -f docker-compose.deploy.yml exec -T labtether sh -c 'cat /bootstrap/auth/setup-token'"
echo "  Docs: https://labtether.com/docs"
echo ""
