#!/usr/bin/env bash
set -euo pipefail
umask 077
fail() { printf '%s\n' "$1" >&2; exit "${2:-1}"; }
usage() { fail 'usage: bootstrap.sh --manifest FILE --manifest-sha256 SHA256 --release-base HTTPS_URL --install-json FILE --directory ABS_PATH --timezone IANA [--update-time HH:MM] [--registry-config FILE] | --configure-registry public|private --directory ABS_PATH [--registry-config FILE]' 2; }
manifest='' expected_manifest='' release_base='' install_json='' directory='' timezone='' update_time='03:30' registry_config=${DNS_REGISTRY_CONFIG_FILE:-} configure_registry=''
while (( $# )); do
  (( $# >= 2 )) || usage
  case $1 in
    --manifest) manifest=$2 ;;
    --manifest-sha256) expected_manifest=$2 ;;
    --release-base) release_base=$2 ;;
    --install-json) install_json=$2 ;;
    --directory) directory=$2 ;;
    --timezone) timezone=$2 ;;
    --update-time) update_time=$2 ;;
    --registry-config) registry_config=$2 ;;
    --configure-registry) configure_registry=$2 ;;
    *) usage ;;
  esac
  shift 2
done
[[ -n $directory ]] || usage
if [[ -n $configure_registry ]]; then
  [[ $configure_registry == public || $configure_registry == private ]] || usage
  [[ -z $manifest && -z $expected_manifest && -z $release_base && -z $install_json ]] || usage
else
  [[ -n $manifest && -n $expected_manifest && -n $release_base && -n $timezone ]] || usage
  [[ $expected_manifest =~ ^[0-9a-f]{64}$ ]] || fail invalid_manifest_checksum
  [[ $release_base =~ ^https://[A-Za-z0-9.-]+(:[0-9]{1,5})?/api/dns/gerenciado/v1/install/releases/[0-9a-f]{40}$ ]] || fail invalid_release_url
fi
[[ $directory == /* && $directory != *'/../'* && $directory != */.. && $directory != *'/./'* && $directory != */. && $directory != / && $directory != *'//'* ]] || fail invalid_directory
if [[ -n $configure_registry ]]; then
  [[ -d $directory && ! -L $directory && $(stat -c '%u:%a' "$directory") == '0:700' && -f $directory/.env && ! -L $directory/.env && $(stat -c '%u:%a:%h' "$directory/.env") == '0:600:1' ]] || fail registry_installation_required
  while IFS='=' read -r key value; do
    case $key in
      TZ) timezone=$value ;;
      WATCHTOWER_SCHEDULE) [[ $value =~ ^0\ ([0-5][0-9])\ ([01][0-9]|2[0-3])\ \*\ \*\ \*$ ]] || fail existing_schedule_mismatch
        update_time="${BASH_REMATCH[2]}:${BASH_REMATCH[1]}" ;;
    esac
  done < "$directory/.env"
fi
[[ $timezone == UTC || $timezone =~ ^[A-Za-z_]+(/[A-Za-z0-9_+-]+)+$ ]] || fail invalid_timezone
[[ $update_time =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]] || fail invalid_update_time
[[ $EUID -eq 0 ]] || fail root_required
for command in curl sha256sum ss docker stat mktemp grep wc flock readlink; do command -v "$command" >/dev/null || fail "missing_prerequisite_$command"; done
[[ $(uname -m) == x86_64 ]] || fail unsupported_architecture
[[ -f /usr/share/zoneinfo/$timezone ]] || fail timezone_unavailable
os_id='' os_version='' os_variant=''
while IFS='=' read -r key value; do
  value=${value#\"}; value=${value%\"}
  case $key in ID) os_id=$value ;; VERSION_ID) os_version=$value ;; VARIANT_ID) os_variant=$value ;; esac
done < /etc/os-release
[[ ( $os_id == fedora && $os_version == 44 && $os_variant == server ) || ( $os_id == debian && $os_version == 13 ) ]] || fail unsupported_host
[[ ${DOCKER_HOST:-} == '' || ${DOCKER_HOST:-} == unix:///var/run/docker.sock ]] || fail docker_remote_context
[[ ${DOCKER_CONTEXT:-} == '' || ${DOCKER_CONTEXT:-} == default ]] || fail docker_remote_context
docker_endpoint=$(docker context inspect default --format '{{ (index .Endpoints "docker").Host }}' 2>/dev/null) || fail docker_context_unavailable
[[ $docker_endpoint == unix:///var/run/docker.sock ]] || fail docker_remote_context
docker() { command docker --context default "$@"; }
docker info >/dev/null 2>&1 || fail docker_unavailable
env -i PATH="$PATH" HOME="${HOME:-/root}" docker --context default compose version >/dev/null 2>&1 || fail compose_unavailable
read -r api_max api_min < <(docker version --format '{{.Server.APIVersion}} {{.Server.MinAPIVersion}}') || fail docker_api_unavailable
[[ $api_max =~ ^[0-9]+\.[0-9]+$ && $api_min =~ ^[0-9]+\.[0-9]+$ ]] || fail docker_api_invalid
api_max_number=$(( ${api_max%.*} * 1000 + ${api_max#*.} ))
api_min_number=$(( ${api_min%.*} * 1000 + ${api_min#*.} ))
(( api_min_number <= 1044 && api_max_number >= 1044 )) || fail docker_api_incompatible
project_hash=$(printf '%s' "$directory" | sha256sum); project_hash=${project_hash%% *}; project="lhisp-dns-${project_hash:0:16}"
hash_file() { local line; line=$(sha256sum -- "$1") || return 1; printf '%s' "${line%% *}"; }
if [[ -z $configure_registry ]]; then
  [[ -f $manifest && ! -L $manifest && $(stat -c %s "$manifest") -le 4096 ]] || fail manifest_missing
  [[ $(hash_file "$manifest") == "$expected_manifest" ]] || fail manifest_checksum_mismatch
fi
read_manifest() {
  local file=$1 key value
  declare -gA release=()
  while IFS='=' read -r key value || [[ -n $key ]]; do
    case $key in schema|channel|sha|version|imageSha|imageTracking|bootstrapSha256|composeSha256) ;; *) fail invalid_manifest_key ;; esac
    [[ -n $value && ! -v release[$key] ]] || fail invalid_manifest_entry
    release[$key]=$value
  done < "$file"
  [[ ${release[schema]:-} == 1 && ${#release[@]} -eq 8 ]] || fail invalid_manifest_schema
  [[ ${release[channel]:-} == development || ${release[channel]:-} == master ]] || fail invalid_manifest_channel
  [[ ${release[sha]:-} =~ ^[0-9a-f]{40}$ && ${release[version]:-} =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]] || fail invalid_manifest_version
  [[ ${release[bootstrapSha256]:-} =~ ^[0-9a-f]{64}$ && ${release[composeSha256]:-} =~ ^[0-9a-f]{64}$ ]] || fail invalid_manifest_digest
  local suffix=''; [[ ${release[channel]} == development ]] && suffix='-dev'
  [[ ${release[imageSha]:-} =~ ^[a-z0-9][a-z0-9_-]{0,39}/lhisp-dns-agent${suffix}:[0-9a-f]{40}$ ]] || fail invalid_manifest_image
  [[ ${release[imageSha]##*:} == "${release[sha]}" && ${release[imageTracking]:-} == "${release[imageSha]%:*}:latest" ]] || fail invalid_manifest_tracking
}
if [[ -z $configure_registry ]]; then
  read_manifest "$manifest"
  [[ $release_base == */"${release[sha]}" ]] || fail release_url_mismatch
  [[ $(hash_file "$0") == "${release[bootstrapSha256]}" ]] || fail bootstrap_checksum_mismatch
fi
[[ ! -L $directory ]] || fail install_directory_symlink
existing=0 completed=0
if [[ -e $directory ]]; then
  [[ -d $directory && $(stat -c '%u:%a' "$directory") == '0:700' ]] || fail install_directory_invalid
  if [[ -f $directory/.env && -f $directory/compose.yaml && -f $directory/release-manifest.txt && -d $directory/state && ( -f $directory/.installed || -f $directory/.installing ) ]]; then
    existing=1
    [[ -f $directory/.installed ]] && completed=1
    [[ ! -L $directory/state && $(stat -c '%u:%a' "$directory/state") == '0:700' ]] || fail state_directory_invalid
  elif [[ -f $directory/.installing && -d $directory/state ]]; then
    [[ ! -L $directory/state && $(stat -c '%u:%a' "$directory/state") == '0:700' ]] || fail partial_state_invalid
  else fail unknown_partial_installation
  fi
  for path in "$directory"/* "$directory"/.[!.]* "$directory"/..?*; do
    [[ -e $path || -L $path ]] || continue
    case ${path##*/} in .env|compose.yaml|release-manifest.txt|state|.installing|.installed|registry-auth) ;; *) fail unknown_install_file ;; esac
  done
else
  [[ -n $install_json ]] || fail install_input_required
fi
if [[ -n $install_json ]]; then
  [[ -f $install_json && ! -L $install_json && $(stat -c '%u:%a' "$install_json") == '0:600' && $(stat -c %s "$install_json") -le 4096 ]] || fail install_input_permissions
fi
registry_overlay_bytes() {
  cat <<'YAML'
services:
  watchtower:
    environment:
      DOCKER_CONFIG: /run/lhisp-registry
    volumes:
      - type: bind
        source: ./registry-auth
        target: /run/lhisp-registry
        read_only: true
        bind:
          create_host_path: false
YAML
}
registry_dir="$directory/registry-auth"
registry_layout() {
  [[ -e $registry_dir || -L $registry_dir ]] || return 0
  [[ -d $registry_dir && ! -L $registry_dir && $(stat -c '%u:%a' "$registry_dir") == '0:700' ]] || fail registry_directory_invalid
  local path expected_overlay
  for path in "$registry_dir"/* "$registry_dir"/.[!.]* "$registry_dir"/..?*; do
    [[ -e $path || -L $path ]] || continue
    case ${path##*/} in schema|config.json|compose.yaml|.config.next) ;; *) fail registry_foreign_file ;; esac
    [[ -f $path && ! -L $path && $(stat -c '%u:%a:%h' "$path") == '0:600:1' && $(stat -c %s "$path") -le 16384 ]] || fail registry_file_invalid
  done
  [[ -f $registry_dir/schema && $(cat "$registry_dir/schema") == lhisp-dns-registry-auth-v1 && -f $registry_dir/config.json && -f $registry_dir/compose.yaml ]] || fail registry_unmanaged_directory
  expected_overlay=$(registry_overlay_bytes | sha256sum)
  [[ $(hash_file "$registry_dir/compose.yaml") == "${expected_overlay%% *}" ]] || fail registry_overlay_changed
}
registry_layout
if [[ -n $registry_config ]]; then
  [[ $registry_config == /* && $registry_config != *:* && $registry_config != *$'\n'* && -f $registry_config && ! -L $registry_config && $(stat -c '%u:%a:%h' "$registry_config") == '0:600:1' && $(stat -c %s "$registry_config") -le 16384 ]] || fail registry_config_permissions
  registry_config=$(readlink -f -- "$registry_config") || fail registry_config_permissions
  if [[ -d $directory ]]; then
    # Existing installations already proved that state is a real directory.
    agent_state_path=$(readlink -f -- "$directory/state") || fail registry_config_permissions
  else
    # Resolve only the existing parent: BusyBox lacks GNU readlink -e/-m.
    registry_install_parent=${directory%/*}; [[ -n $registry_install_parent ]] || registry_install_parent=/
    [[ -d $registry_install_parent ]] || fail registry_config_permissions
    registry_install_parent=$(readlink -f -- "$registry_install_parent") || fail registry_config_permissions
    agent_state_path="${registry_install_parent%/}/${directory##*/}/state"
  fi
  case $registry_config in "$agent_state_path"|"$agent_state_path"/*) fail registry_config_inside_agent_state ;; esac
  registry_source_parent=${registry_config%/*}; [[ -n $registry_source_parent ]] || registry_source_parent=/
  [[ $(stat -c %u -- "$registry_source_parent") == "$EUID" ]] || fail registry_config_parent_permissions
  registry_parent_mode=$(stat -c %a -- "$registry_source_parent") || fail registry_config_parent_permissions
  [[ $registry_parent_mode =~ ^[0-7]{3,4}$ ]] || fail registry_config_parent_permissions
  (( (8#$registry_parent_mode & 0022) == 0 )) || fail registry_config_parent_permissions
fi
[[ $configure_registry != private || -n $registry_config ]] || fail registry_config_required
[[ $configure_registry != public || -z $registry_config ]] || fail registry_config_not_used_in_public_mode
compose_local() {
  local overlays=()
  [[ ! -d $registry_dir ]] || overlays=(-f "$registry_dir/compose.yaml")
  env -i PATH="$PATH" HOME="${HOME:-/root}" DNS_IMAGE="${release[imageTracking]}" WATCHTOWER_SCOPE="$project" WATCHTOWER_SCHEDULE="$schedule" TZ="$timezone" docker --context default compose --project-name "$project" --project-directory "$directory" --env-file "$directory/.env" -f "$directory/compose.yaml" "${overlays[@]}" "$@"
}
own_dns_running() {
  local container actual
  container=$(compose_local ps --status running -q dns 2>/dev/null) || return 1
  [[ -n $container ]] || return 1
  actual=$(docker inspect --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' "$container" 2>/dev/null) || return 1
  [[ $actual == "$directory" ]]
}
owned_container() {
  local service=$1 container ownership
  container=$(compose_local ps --all -q "$service") || return 1
  [[ -n $container && $container != *$'\n'* ]] || return 1
  ownership=$(docker inspect --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}|{{index .Config.Labels "com.docker.compose.service"}}|{{index .Config.Labels "com.docker.compose.project"}}' "$container") || return 1
  [[ $ownership == "$directory|$service|$project" ]] || return 1
  printf '%s' "$container"
}
# Lock by installation parent; the inode is retained so independent invocations serialize.
parent=${directory%/*}; [[ -n $parent ]] || parent=/
[[ -d $parent && ! -L $parent ]] || fail install_parent_invalid
parent_owner=$(stat -c %u -- "$parent") || fail install_parent_invalid
parent_mode=$(stat -c %a -- "$parent") || fail install_parent_invalid
[[ $parent_owner == "$EUID" && $parent_mode =~ ^[0-7]{3,4}$ ]] || fail install_parent_invalid
(( (8#$parent_mode & 0022) == 0 )) || fail install_parent_invalid
lock_path="$parent/.lhisp-dns-bootstrap.lock"
if [[ -e $lock_path || -L $lock_path ]]; then
  [[ -f $lock_path && ! -L $lock_path && $(stat -c '%u:%a:%h' -- "$lock_path") == "$EUID:600:1" ]] || fail bootstrap_lock_invalid
fi
# O pai é privado ou não gravável por terceiros. Abrir em append preserva o inode e o conteúdo.
exec {bootstrap_lock_fd}>> "$lock_path" || fail bootstrap_lock_invalid
[[ -f /proc/$$/fd/$bootstrap_lock_fd && $(stat -Lc '%u:%a:%h:%d:%i' -- "/proc/$$/fd/$bootstrap_lock_fd") == "$EUID:600:1:$(stat -c '%d:%i' -- "$lock_path")" && ! -L $lock_path ]] || fail bootstrap_lock_invalid
flock -n "$bootstrap_lock_fd" || fail bootstrap_busy
registry_layout
if (( ! existing )) && [[ -e $directory && ! -f $directory/.installing ]]; then fail concurrent_install_changed; fi
if (( existing && ! completed )) && [[ -f $directory/.installed ]]; then fail concurrent_install_changed; fi
# All host prerequisites and trust checks precede state/container changes.
if (( existing )); then
  [[ -f $directory/release-manifest.txt && ! -L $directory/release-manifest.txt && $(stat -c %s "$directory/release-manifest.txt") -le 4096 ]] || fail stored_manifest_missing
  read_manifest "$directory/release-manifest.txt"
  [[ $(stat -c %s "$directory/compose.yaml") -le 65536 && $(hash_file "$directory/compose.yaml") == "${release[composeSha256]}" ]] || fail existing_compose_changed
  [[ -f $directory/.env && ! -L $directory/.env && $(stat -c '%u:%a' "$directory/.env") == '0:600' ]] || fail existing_env_invalid
  [[ $(stat -c '%u:%a' "$directory/compose.yaml") == '0:600' && $(stat -c '%u:%a' "$directory/release-manifest.txt") == '0:600' ]] || fail existing_files_invalid
  grep -Fxq "DNS_IMAGE=${release[imageTracking]}" "$directory/.env" || fail existing_image_mismatch
  grep -Fxq "TZ=$timezone" "$directory/.env" || fail existing_timezone_mismatch
  schedule="0 ${update_time#*:} ${update_time%:*} * * *"
  grep -Fxq "WATCHTOWER_SCHEDULE=$schedule" "$directory/.env" || fail existing_schedule_mismatch
  local_scope=$(printf '%s' "$directory" | sha256sum); local_scope=${local_scope%% *}; local_scope="lhisp-dns-${local_scope:0:16}"
  [[ $local_scope == "$project" ]] || fail existing_project_mismatch
  grep -Fxq "WATCHTOWER_SCOPE=$local_scope" "$directory/.env" || fail existing_scope_mismatch
  [[ $(wc -l < "$directory/.env") -eq 4 ]] || fail existing_env_extra
fi
[[ -z $configure_registry || $completed == 1 ]] || fail registry_installation_required
if (( ! completed )); then
  listeners=$(ss -H -ltnu 'sport = :53') || fail port_check_failed
  if [[ -n $listeners ]]; then
    if (( ! existing )) || ! own_dns_running; then fail dns_port_in_use; fi
  fi
fi
image_source=${release[imageSha]}
image_cli=${release[imageTracking]}
if (( completed )); then
  container=$(owned_container dns) || fail installed_container_missing
  image_cli=$(docker inspect --format '{{.Image}}' "$container") || fail installed_image_unavailable
  [[ $image_cli =~ ^sha256:[0-9a-f]{64}$ && $(docker image inspect --format '{{.Id}}' "$image_cli") == "$image_cli" && $(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image_cli") == linux/amd64 ]] || fail installed_image_unavailable
elif (( ! existing )); then
  if ! docker image inspect "$image_source" >/dev/null 2>&1; then docker pull "$image_source" >/dev/null || fail image_pull_failed; fi
  docker tag "$image_source" "${release[imageTracking]}" || fail image_tag_failed
elif ! docker image inspect "${release[imageTracking]}" >/dev/null 2>&1; then
  if own_dns_running; then fail image_unavailable_preserving_running_dns 2; fi
  docker pull "$image_source" >/dev/null || fail image_pull_failed
  docker tag "$image_source" "${release[imageTracking]}" || fail image_tag_failed
fi
# Validate credentials in an isolated local image; no host Node/jq/helper dependency.
registry_stage=''
trap '[[ -z $registry_stage ]] || rm -rf -- "$registry_stage"' EXIT
validate_registry_config() {
  local access=${2:-ro} normalize=${3:-check}
  docker run --rm --pull never --network none --read-only --cap-drop ALL --security-opt no-new-privileges -v "$1:/run/registry:$access" --entrypoint node "$image_cli" -e '
const fs=require("node:fs");
try {
  const raw=fs.readFileSync("/run/registry/config.json","utf8"), c=JSON.parse(raw);
  const exact=(o,keys)=>o&&typeof o==="object"&&!Array.isArray(o)&&Object.keys(o).sort().join("|")===keys.sort().join("|");
  if(!exact(c,["auths"])||!exact(c.auths,["https://index.docker.io/v1/"]))process.exit(1);
  const h=c.auths["https://index.docker.io/v1/"];
  if(!exact(h,["auth"])||typeof h.auth!=="string"||h.auth.length>8192||!/^[A-Za-z0-9+/]+={0,2}$/.test(h.auth))process.exit(1);
  const b=Buffer.from(h.auth,"base64");
  if(b.toString("base64")!==h.auth||!(/^[A-Za-z0-9_-]{1,128}:[\x21-\x7e]+$/).test(b.toString("utf8")))process.exit(1);
  const canonical=JSON.stringify({auths:{"https://index.docker.io/v1/":{auth:h.auth}}})+"\n";
  if(process.argv[1]==="normalize")fs.writeFileSync("/run/registry/config.json",canonical,{mode:0o600});
  else if(raw!==canonical)process.exit(1);
} catch { process.exit(1); }
' "$normalize" >/dev/null 2>&1 || fail registry_config_invalid
}
if [[ -n $registry_config ]]; then
  registry_stage=$(mktemp -d "$parent/.lhisp-dns-registry.XXXXXX")
  cp -- "$registry_config" "$registry_stage/config.json"; chmod 0600 "$registry_stage/config.json"
  validate_registry_config "$registry_stage" rw normalize
fi
if [[ -d $registry_dir && -z $configure_registry ]]; then validate_registry_config "$registry_dir"; fi
save_registry_config() {
  if [[ -d $registry_dir ]]; then
    cp -- "$registry_stage/config.json" "$registry_dir/.config.next"; chmod 0600 "$registry_dir/.config.next"
    mv -- "$registry_dir/.config.next" "$registry_dir/config.json"
  else
    registry_overlay_bytes > "$registry_stage/compose.yaml"
    printf '%s\n' lhisp-dns-registry-auth-v1 > "$registry_stage/schema"
    chmod 0600 "$registry_stage/compose.yaml" "$registry_stage/schema"
    mv -- "$registry_stage" "$registry_dir"; registry_stage=''
  fi
}
if [[ -n $configure_registry ]]; then
  updater=$(owned_container watchtower) || fail watchtower_container_missing
  updater_state=$(docker inspect --format '{{.State.Running}}|{{.State.Paused}}' "$updater") || fail watchtower_state_unavailable
  [[ $updater_state == true\|false || $updater_state == false\|false ]] || fail watchtower_paused_preserved
  if [[ $configure_registry == private ]]; then
    save_registry_config
  elif [[ -d $registry_dir ]]; then
    registry_stage=$(mktemp -d "$parent/.lhisp-dns-registry.XXXXXX")
    mv -- "$registry_dir" "$registry_stage/previous"
  fi
  compose_local config -q || fail registry_configuration_saved_compose_invalid
  updater_start=(); [[ $updater_state == true\|false ]] || updater_start=(--no-start)
  compose_local up -d --no-deps --force-recreate --pull never "${updater_start[@]}" watchtower || fail registry_configuration_saved_updater_recreate_failed
  printf '%s\n' 'Registry configurado somente no Watchtower; DNS preservado. Autenticação remota não verificada.'
  exit 0
fi
if (( completed )) && [[ -n $registry_config ]]; then
  [[ -d $registry_dir && $(hash_file "$registry_stage/config.json") == "$(hash_file "$registry_dir/config.json")" ]] || fail registry_configuration_mode_required
fi

if [[ -n $install_json ]]; then
  tenant_origin=${release_base%%/api/*}
  docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges -v "$install_json:/run/install.json:ro" --entrypoint node "$image_cli" -e 'const b=require("/app/dist/binding");const c=b.parseInstall(b.readProtected("/run/install.json"));if(!c||c.endpoint!==process.argv[1])process.exit(1)' "$tenant_origin" || fail install_origin_mismatch
fi
if (( ! existing )); then
  if [[ ! -e $directory ]]; then mkdir -m 0700 -- "$directory"; fi
  if [[ ! -e $directory/state ]]; then mkdir -m 0700 -- "$directory/state"; fi
  : > "$directory/.installing"; chmod 0600 "$directory/.installing"
  temp=$(mktemp "$directory/state/.compose.XXXXXX")
  trap 'rm -f -- "$temp"; [[ -z $registry_stage ]] || rm -rf -- "$registry_stage"' EXIT
  curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-redirs 0 --connect-timeout 5 --max-time 20 --max-filesize 65536 --output "$temp" "$release_base/compose.yaml" || fail compose_download_failed
  [[ $(stat -c %s "$temp") -le 65536 && $(hash_file "$temp") == "${release[composeSha256]}" ]] || fail compose_checksum_mismatch
  chmod 0600 "$temp"; mv -- "$temp" "$directory/compose.yaml"; trap '[[ -z $registry_stage ]] || rm -rf -- "$registry_stage"' EXIT
  cp -- "$manifest" "$directory/state/.manifest.next"; chmod 0600 "$directory/state/.manifest.next"; mv -- "$directory/state/.manifest.next" "$directory/release-manifest.txt"
  local_scope=$project
  schedule="0 ${update_time#*:} ${update_time%:*} * * *"
  printf 'DNS_IMAGE=%s\nWATCHTOWER_SCOPE=%s\nWATCHTOWER_SCHEDULE=%s\nTZ=%s\n' "${release[imageTracking]}" "$local_scope" "$schedule" "$timezone" > "$directory/state/.env.next"
  chmod 0600 "$directory/state/.env.next"; mv -- "$directory/state/.env.next" "$directory/.env"
fi
if (( ! completed )) && [[ -n $registry_config ]]; then save_registry_config; fi
compose_local config -q || fail compose_config_invalid
# Maintenance: never consume a new authorization while the installed DNS is stopped.
if (( completed )); then own_dns_running || fail dns_stopped_preserved 2; fi
if [[ -n $install_json ]]; then
  copied_input=0
  if [[ $install_json != "$directory/state/install.json" ]]; then
    [[ ! -e $directory/state/install.json && ! -L $directory/state/install.json ]] || fail install_state_input_exists
    cp -- "$install_json" "$directory/state/install.json"; chmod 0600 "$directory/state/install.json"
    copied_input=1
  fi
  if ! docker run --rm --network host --read-only --cap-drop ALL --security-opt no-new-privileges -v "$directory/state:/var/lib/lhisp-dns" --entrypoint node "$image_cli" /app/dist/enroll.js /var/lib/lhisp-dns/install.json; then
    if (( copied_input )); then rm -f -- "$directory/state/install.json"; fi
    fail enrollment_incomplete
  fi
  if (( copied_input )); then rm -- "$install_json"; fi
elif [[ ! -f $directory/state/credential.json ]]; then
  fail install_input_required
else
  tenant_origin=${release_base%%/api/*}
  docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges -v "$directory/state:/var/lib/lhisp-dns:ro" --entrypoint node "$image_cli" -e 'const b=require("/app/dist/binding");const c=b.readBinding("/var/lib/lhisp-dns"),l=require("/app/dist/lifecycle").readLifecycle("/var/lib/lhisp-dns");if(!c||!l||l.state!=="active"||l.generation!==c.geracao||c.endpoint!==process.argv[1])process.exit(1)' "$tenant_origin" || fail existing_origin_mismatch
fi
if (( ! completed )); then
  compose_local up -d --no-recreate || fail compose_start_failed
  : > "$directory/.installed"; chmod 0600 "$directory/.installed"
  rm -f -- "$directory/.installing"
else
  own_dns_running || fail dns_stopped_preserved 2
fi
if [[ ! -f $directory/state/unbound.conf ]]; then
  fail aguardando_configuracao_dns 2
fi
docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges -v "$directory/state:/var/lib/lhisp-dns:ro" --entrypoint unbound-checkconf "$image_cli" /var/lib/lhisp-dns/unbound.conf >/dev/null 2>&1 || fail configuracao_dns_invalida 2
printf '%s\n' 'Instalação iniciada; configuração Unbound válida. Saúde DNS ainda não verificada.'
