# shellcheck shell=bash
#
# Shared config loader for every build script in tools/. Source it, call
# `core_load`, and every CORE_* variable is in scope.
#
#   source "$(dirname "${BASH_SOURCE[0]}")/lib/coreconfig.sh"
#   core_load                      # core values only
#   core_load --kiosk door-panel   # + that kiosk's own values
#
# WHY THIS EXISTS: before it, every builder carried its own copy of the container
# host's IP, the MQTT port, the identity token and half a dozen URLs. Six scripts, six
# chances to typo one of them, and the symptom was always the same — a kiosk that
# boots fine and then can't reach something, discovered after a 40-minute ISO build.
# Now no build script contains an address at all; they all read one file, and
# validate-config.py has already refused the build if that file is wrong.
#
# Nothing here is exported to child processes on purpose: build scripts substitute
# these into generated files explicitly, so it's always visible in the script which
# value went where.

CORE_REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CORE_CONFIG_PATH="${CORE_CONFIG_PATH:-${CORE_REPO_ROOT}/CoreSystemConfig.json}"
CORE_TOOLS_DIR="${CORE_REPO_ROOT}/tools"

core_die() { echo -e "\033[1;31m[error]\033[0m $*" >&2; exit 1; }
core_log() { echo -e "\n\033[1;34m==>\033[0m $*"; }
core_warn() { echo -e "\033[1;33m[warn]\033[0m $*" >&2; }

core_load() {
  if [[ ! -f "$CORE_CONFIG_PATH" ]]; then
    core_die "No CoreSystemConfig.json found at $CORE_CONFIG_PATH

  Create one from the template and fill it in:
      cp ${CORE_REPO_ROOT}/CoreSystemConfig.json.template ${CORE_REPO_ROOT}/CoreSystemConfig.json
      \$EDITOR ${CORE_REPO_ROOT}/CoreSystemConfig.json

  Then check it with:
      ${CORE_TOOLS_DIR}/validate-config.py

  (Set CORE_CONFIG_PATH to build from a different file — useful for a second
   household or a test config.)"
  fi

  command -v python3 >/dev/null 2>&1 || core_die "python3 is required to read CoreSystemConfig.json"

  # Fill in any empty service tokens BEFORE validating, since the validator's whole
  # job is to refuse a build with empty ones. Only fills blanks, never overwrites — see
  # generate-tokens.py for why persisting them matters rather than generating per build.
  core_log "Checking secrets"
  python3 "${CORE_TOOLS_DIR}/generate-tokens.py" "$CORE_CONFIG_PATH" \
    || core_die "Could not generate tokens into $CORE_CONFIG_PATH"

  # VALIDATE BEFORE ANYTHING ELSE. An ISO build is long and mostly unattended; the
  # entire value of the validator evaporates if it runs after 40 minutes of debootstrap
  # rather than before it. Warnings print but don't stop the build.
  core_log "Validating $(basename "$CORE_CONFIG_PATH")"
  if ! python3 "${CORE_TOOLS_DIR}/validate-config.py" "$CORE_CONFIG_PATH"; then
    core_die "CoreSystemConfig.json has errors (above) — fix them before building.
     Nothing was built and nothing was written."
  fi

  local exported
  if ! exported="$(python3 "${CORE_TOOLS_DIR}/config-export.py" "$CORE_CONFIG_PATH" "$@")"; then
    # config-export.py emits a shell `echo ... >&2; exit 1` on an unknown kiosk, so
    # evaluating its output is what surfaces that message.
    eval "$exported"
    core_die "Could not read $CORE_CONFIG_PATH"
  fi
  # Every value is shlex.quote'd on the Python side, so a password containing quotes,
  # spaces or semicolons survives this intact rather than becoming shell syntax.
  eval "$exported"
}

# Load the one kiosk this builder is for.
#   core_select_kiosk door-panel            # the only door-panel in the config
#   core_select_kiosk thin-client living    # a specific one, when there are several
#
# Taking the hostname as an argument (rather than a per-script constant) is what lets
# one builder produce several images: two thin clients in two rooms are two entries in
# CoreSystemConfig.json, not two copies of a script with one line changed — which is
# how the addresses drifted apart in the first place.
core_select_kiosk() {
  local want_type="$1" want_host="${2:-}"
  local matches
  matches="$(python3 - "$CORE_CONFIG_PATH" "$want_type" <<'PY'
import json, sys
cfg = json.load(open(sys.argv[1]))
print("\n".join(k["hostname"] for k in cfg.get("kiosks", []) if k.get("type") == sys.argv[2]))
PY
)"

  if [[ -z "$matches" ]]; then
    core_die "No kiosk of type '${want_type}' in $(basename "$CORE_CONFIG_PATH").
     Add one to the \"kiosks\" list and re-run."
  fi

  if [[ -z "$want_host" ]]; then
    if [[ "$(wc -l <<< "$matches")" -gt 1 ]]; then
      core_die "Several '${want_type}' kiosks are configured — say which one:
$(sed 's/^/       /' <<< "$matches")

     e.g. $0 $(head -1 <<< "$matches")"
    fi
    want_host="$matches"
  elif ! grep -qx "$want_host" <<< "$matches"; then
    core_die "'${want_host}' is not a configured ${want_type}. Available:
$(sed 's/^/       /' <<< "$matches")"
  fi

  core_load --kiosk "$want_host"
}

# Same idea as core_select_kiosk, for the headless audio endpoints — selected by
# architecture, since those are two genuinely different builders (live-build for the
# amd64 mini-PC, rpi-image-gen for the arm64 Pi) rather than one with a flag.
core_select_audio_endpoint() {
  local want_arch="$1" want_host="${2:-}"
  local matches
  matches="$(python3 - "$CORE_CONFIG_PATH" "$want_arch" <<'PY'
import json, sys
cfg = json.load(open(sys.argv[1]))
print("\n".join(a["hostname"] for a in cfg.get("audio_endpoints", []) if a.get("arch") == sys.argv[2]))
PY
)"

  if [[ -z "$matches" ]]; then
    core_die "No ${want_arch} audio endpoint in $(basename "$CORE_CONFIG_PATH").
     Add one to the \"audio_endpoints\" list and re-run."
  fi

  if [[ -z "$want_host" ]]; then
    if [[ "$(wc -l <<< "$matches")" -gt 1 ]]; then
      core_die "Several ${want_arch} audio endpoints are configured — say which one:
$(sed 's/^/       /' <<< "$matches")"
    fi
    want_host="$matches"
  elif ! grep -qx "$want_host" <<< "$matches"; then
    core_die "'${want_host}' is not a configured ${want_arch} audio endpoint. Available:
$(sed 's/^/       /' <<< "$matches")"
  fi

  core_load --audio-endpoint "$want_host"
}

# Deterministic identifier for a matched set of images, printed at build time and
# written into every image as /etc/smarthome-build. Two ISOs built from the same
# config carry the same PAIR_ID; if you ever end up holding a container-host ISO and
# an llm-host ISO and can't remember whether they agree on addresses and tokens,
# compare this and you know.
core_pair_id() {
  local hash
  hash="$(python3 - "$CORE_CONFIG_PATH" <<'PY'
import hashlib, json, sys
# Hash the SEMANTIC content, not the bytes: reformatting the JSON or reordering keys
# must not change the pair ID, because it didn't change what gets built.
cfg = json.loads(open(sys.argv[1]).read())
def strip(node):
    if isinstance(node, dict):
        return {k: strip(v) for k, v in sorted(node.items()) if not k.startswith("_")}
    if isinstance(node, list):
        return [strip(v) for v in node]
    return node
print(hashlib.sha256(json.dumps(strip(cfg), sort_keys=True).encode()).hexdigest()[:12])
PY
)"
  echo "$hash"
}

# Written into every image so a booted machine can say what it was built from.
core_write_build_stamp() {
  local dest="$1" role="$2"
  mkdir -p "$(dirname "$dest")"
  cat > "$dest" <<EOF
# Generated by tools/ at image build time — see tools/README.md
SMARTHOME_ROLE=${role}
SMARTHOME_PAIR_ID=$(core_pair_id)
SMARTHOME_BUILT_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
SMARTHOME_CONTAINER_HOST=${CORE_CONTAINER_HOST_IP}
SMARTHOME_LLM_HOST=${CORE_LLM_HOST_IP}
EOF
}

# Move a finished image out of its live-build tree into iso-out/, named for the
# thing it actually is.
#
# THIS IS LOAD-BEARING FOR MULTI-INSTANCE BUILDS, not cosmetic. live-build always
# writes the same filename (live-image-amd64.hybrid.iso) into the same per-host tree,
# so building two thin clients — a living-room one and a bedroom one — would have the
# second silently overwrite the first, leaving one ISO carrying the second room's
# hostname and no indication the first was ever lost. Publishing under
# <role>-<hostname>-<pairid> makes each config entry produce its own artifact.
core_publish_image() {
  local src="$1" role="$2" instance="${3:-}"
  local out_dir="${CORE_REPO_ROOT}/${CORE_BUILD_OUTPUT_DIR}"
  mkdir -p "$out_dir"

  # Hostnames usually already carry the role ("thin-client-bedroom"), so appending the
  # role verbatim gives "thin-client-thin-client-bedroom". Use the hostname alone when
  # it already starts with the role.
  local name="smarthome-${role}"
  if [[ -n "$instance" ]]; then
    if [[ "$instance" == "$role"* ]]; then
      name="smarthome-${instance}"
    else
      name="smarthome-${role}-${instance}"
    fi
  fi
  local dest="${out_dir}/${name}-$(core_pair_id).${src##*.}"

  if [[ ! -f "$src" ]]; then
    core_warn "Expected an image at ${src} but found none — nothing published."
    return 1
  fi
  mv "$src" "$dest"
  core_log "Image: ${dest}"
  echo "$dest"
}

# Guard for the ISO builders: `lb build` needs root, and finding that out after the
# config phase has already written files is worse than finding out now.
core_require_root() {
  [[ $EUID -eq 0 ]] || core_die "This needs root (lb build does). Re-run with: sudo -E $0 $*"
}
