#!/usr/bin/env bash
#
# Smart Home Container Host Setup
# Target: Debian 12 (Bookworm) or Raspberry Pi OS Lite (Bookworm-based)
#
# Sets up Docker + a compose stack for everything that lives on this single
# host per docs/project-plan.md Phase 1 and Phase 9 (the LLM/GPU host is a
# separate physical machine — see hosts/llm-host):
#   - Home Assistant (Container install)
#   - Mosquitto (MQTT broker)
#   - Zigbee2MQTT (talks to your USB-attached CC2652P/Haozee coordinator)
#   - Node-RED (automation glue, identity-correlation flow home)
#   - Frigate (NVR + face recognition, for the peephole cam)
#   - Grocy (self-hosted inventory / shopping list)
#   - Mealie (optional meal planning)
#   - Netdata (host + container monitoring)
#   - Homepage (single dashboard landing page)
#   - ntfy (self-hosted push notifications)
#   - Portainer (Docker GUI)
#   - gallery-smb (optional, off by default — read-only SMB share of photos for the
#     Phase 11 thin clients' idle slideshow)
#   - restic scheduled backups (optional, off by default)
#   - digest-engine + digest-web (Phase 12 quarter-daily LLM digest, optional,
#     off by default — needs digest-engine/ from this repo checked out on this
#     host, see DIGEST_ENGINE_SRC below and digest-engine/README.md)
#   - whatsapp-bridge (optional, off by default, gated separately — real ban
#     risk, see digest-engine/README.md before enabling)
#   - admin-canvas + admin-web (Phase 13 sys-admin-llm display surface, optional,
#     off by default — needs admin-canvas/ from this repo checked out on this
#     host, see ADMIN_CANVAS_SRC below and admin-canvas/README.md)
#   - pantry-vision + pantry-web (Phase 17 kitchen-display camera cataloguing
#     backend, optional, off by default — needs pantry-vision/ from this repo
#     checked out on this host, see PANTRY_VISION_SRC below and
#     pantry-vision/README.md)
#   - identity + identity-web (Phase 6 person <-> BLE-identifier registry —
#     multi-phone support, anti-spoofing registration, presence/weather for
#     hosts/door-panel's and hosts/kitchen-display's dashboards — optional, off by
#     default, needs identity/ from this repo checked out on this host, see
#     IDENTITY_SRC below and identity/README.md)
#   - trash-calendar (Phase 19, optional, off by default — syncs Kennelbach's
#     personal collection-date ICS feed into the household's own Nextcloud
#     calendar, daily via systemd timer, needs trash-calendar/ from this repo
#     checked out on this host, see TRASH_CALENDAR_SRC below and
#     trash-calendar/README.md)
#   - transit + optional otp (Phase 19, optional, off by default — "when's the
#     next bus" voice lookups from a weekly-refreshed GTFS feed, plus optional
#     on-demand route planning via a self-hosted OpenTripPlanner; needs transit/
#     from this repo checked out on this host, see TRANSIT_SRC below and
#     transit/README.md)
#
# Run as: sudo ./setup-container-host.sh
#
# EDIT THE VARIABLES BELOW BEFORE RUNNING.

set -euo pipefail

# ---------------------------------------------------------------------------
# CONFIGURATION
#
# Every value here is `${VAR:-default}`, so anything already in the environment wins.
# That is how the ISO built by tools/build-container-host-iso.sh configures this
# script without editing it: the first-boot unit sources a generated env file derived
# from CoreSystemConfig.json, and these defaults apply only to a hand-run install.
# Edit them directly ONLY if you are running this script standalone.
# ---------------------------------------------------------------------------
BASE_DIR="${BASE_DIR:-/opt/smart-home}"          # Where all container config/data will live
TIMEZONE="${TIMEZONE:-Europe/Vienna}"            # Adjust to your timezone
ENABLE_MEALIE="${ENABLE_MEALIE:-false}"                # Set to "true" to also deploy Mealie
ENABLE_INTEL_HWACCEL="${ENABLE_INTEL_HWACCEL:-false}"         # Set to "true" if this host has an Intel iGPU for Frigate

# --- Phase 1/9 add-ons — on by default, set to "false" to skip any of them ---
ENABLE_NODERED="${ENABLE_NODERED:-true}"
ENABLE_NETDATA="${ENABLE_NETDATA:-true}"
ENABLE_HOMEPAGE="${ENABLE_HOMEPAGE:-true}"
ENABLE_NTFY="${ENABLE_NTFY:-true}"
ENABLE_PORTAINER="${ENABLE_PORTAINER:-true}"

# --- Gallery SMB share — off by default until a password is chosen ----------
# Serves $BASE_DIR/gallery read-only over SMB so idle thin clients cycle through photos
# instead of blanking (Phase 11). Unlike the restic password below, this one is NOT
# auto-generated: the identical value has to be typed into
# /etc/thinclient-agent/gallery-credentials on every thin client, so a secret only this
# script ever saw would be a secret the other end cannot have. Pick one yourself.
ENABLE_GALLERY_SMB="${ENABLE_GALLERY_SMB:-false}"
GALLERY_SMB_USERNAME="${GALLERY_SMB_USERNAME:-gallery}"
GALLERY_SMB_PASSWORD="${GALLERY_SMB_PASSWORD:-}"             # <-- SET THIS before flipping the toggle above

# --- Photo web frontend (Immich) — browsing the same photos the SMB share serves ---
# The share is right for bulk copy and for keeping the files openable without any of
# this; it is hopeless for FINDING a photo. Immich adds search, albums, timeline and
# faces over the same directory. Off by default: it brings a Postgres and a Redis with
# it, which is the largest single addition to this stack's footprint.
ENABLE_PHOTOS_WEB="${ENABLE_PHOTOS_WEB:-false}"
PHOTOS_WEB_PORT="${PHOTOS_WEB_PORT:-2283}"
PHOTOS_DB_PASSWORD="${PHOTOS_DB_PASSWORD:-}"                 # <-- SET THIS before flipping the toggle above

# --- Workshop assistant (project notebook + knowledge store + health poller) ---
ENABLE_WORKSHOP="${ENABLE_WORKSHOP:-false}"
WORKSHOP_PORT="${WORKSHOP_PORT:-8102}"
WORKSHOP_WEB_PORT="${WORKSHOP_WEB_PORT:-8103}"
WORKSHOP_SRC="${WORKSHOP_SRC:-/opt/smart-home/src/workshop}"
WORKSHOP_SMB_USERNAME="${WORKSHOP_SMB_USERNAME:-workshop}"
WORKSHOP_SMB_PASSWORD="${WORKSHOP_SMB_PASSWORD:-}"           # <-- SET THIS before flipping the toggle above

# --- Scheduled backups (restic) — off by default until you pick a target ---
# Set ENABLE_BACKUPS=true and RESTIC_REPOSITORY to a local path (e.g. an
# external/USB drive mount, or a NAS mount), or a remote target restic
# supports (s3:..., sftp:..., b2:..., rest:...). See https://restic.net
ENABLE_BACKUPS="${ENABLE_BACKUPS:-false}"
RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-/mnt/backup/smart-home-restic}"
BACKUP_SCHEDULE="${BACKUP_SCHEDULE:-03:30}"             # systemd OnCalendar time, daily at this local time

# --- Zigbee adapter: USB (CC2652P/CH340C, e.g. Haozee/Sonoff Dongle-P style) ---
# Run `ls -l /dev/serial/by-id/` AFTER plugging the adapter in, and paste the
# full path it shows here. This is more stable across reboots than /dev/ttyUSB0.
# Example: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0
ZIGBEE_USB_DEVICE="${ZIGBEE_USB_DEVICE:-/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0}"

# --- Quarter-daily LLM digest (Phase 12) — off by default until credentials
# --- are provisioned. See digest-engine/README.md.
ENABLE_DIGEST_ENGINE="${ENABLE_DIGEST_ENGINE:-false}"
# WhatsApp ingestion is separately gated and highest-risk of the four message
# platforms. Read digest-engine/README.md before setting this to "true" — real
# ban risk even with the headful-Chromium mitigation; use a secondary number.
ENABLE_WHATSAPP_INGEST="${ENABLE_WHATSAPP_INGEST:-false}"
# Where this repo's digest-engine/ directory lives on THIS host (build context).
DIGEST_ENGINE_SRC="${DIGEST_ENGINE_SRC:-/opt/smart-home/src/digest-engine}"
DIGEST_WEB_PORT="${DIGEST_WEB_PORT:-8091}"             # LAN-facing read-only static serving
DIGEST_SCHEDULE="${DIGEST_SCHEDULE:-00,06,12,18}"      # systemd OnCalendar hours, 4x/day

# --- On-demand sys-admin-llm display surface (Phase 13) — off by default until
# --- ADMIN_CANVAS_TOKEN is provisioned. See admin-canvas/README.md.
ENABLE_ADMIN_CANVAS="${ENABLE_ADMIN_CANVAS:-false}"
# Where this repo's admin-canvas/ directory lives on THIS host (build context).
ADMIN_CANVAS_SRC="${ADMIN_CANVAS_SRC:-/opt/smart-home/src/admin-canvas}"
ADMIN_CANVAS_PORT="${ADMIN_CANVAS_PORT:-8092}"           # internal only — no `ports:` mapping, HA-reachable only
ADMIN_WEB_PORT="${ADMIN_WEB_PORT:-8094}"              # LAN-facing read-only static serving

# --- Kitchen-display camera cataloguing backend (Phase 17) — off by default until
# --- PANTRY_VISION_TOKEN and GROCY_API_KEY are provisioned. See pantry-vision/README.md.
# Unlike admin-canvas, this one IS meant to be reached directly by a separate physical
# device (hosts/kitchen-display's kiosk browser), not only by Home Assistant — so,
# deliberately unlike ADMIN_CANVAS_PORT above, it DOES get a `ports:` mapping. The
# bearer token is the actual boundary here, not network placement — see
# pantry-vision/README.md's "A real network listener, unlike admin-canvas" section.
ENABLE_PANTRY_VISION="${ENABLE_PANTRY_VISION:-false}"
# Where this repo's pantry-vision/ directory lives on THIS host (build context).
PANTRY_VISION_SRC="${PANTRY_VISION_SRC:-/opt/smart-home/src/pantry-vision}"
PANTRY_VISION_PORT="${PANTRY_VISION_PORT:-8095}"          # LAN-facing — the kitchen display's kiosk browser calls this directly
PANTRY_WEB_PORT="${PANTRY_WEB_PORT:-8096}"             # LAN-facing read-only static serving (the kiosk's frontend)

# --- Person <-> BLE-identifier registry (Phase 6) — off by default until
# --- IDENTITY_TOKEN, HA_TOKEN, and TRUSTED_ENTITY_PREFIXES are provisioned. See
# --- identity/README.md. Same "published, unlike admin-canvas" reasoning as
# --- ENABLE_PANTRY_VISION above — hosts/kitchen-display's and hosts/door-panel's
# --- kiosk browsers call this directly.
ENABLE_IDENTITY="${ENABLE_IDENTITY:-false}"

# --- Reverse proxy (Caddy) — one HTTPS front door for this repo's own services, plus
# --- an HTTP->HTTPS redirect. See proxy/README.md for why it isn't just hygiene: the
# --- admin panel's token rides in a URL, getUserMedia needs a secure context, and a
# --- mixed-content page can't call its own API. Home Assistant and the rest keep their
# --- own ports and stay off it.
ENABLE_PROXY="${ENABLE_PROXY:-false}"
PROXY_HOSTNAME="${PROXY_HOSTNAME:-}"
PROXY_TLS="${PROXY_TLS:-internal}"        # internal | custom
PROXY_CERT_FILE="${PROXY_CERT_FILE:-}"    # host paths, only used when PROXY_TLS=custom
PROXY_KEY_FILE="${PROXY_KEY_FILE:-}"
PROXY_HTTP_PORT="${PROXY_HTTP_PORT:-80}"
PROXY_HTTPS_PORT="${PROXY_HTTPS_PORT:-443}"
# Where this repo's identity/ directory lives on THIS host (build context).
IDENTITY_SRC="${IDENTITY_SRC:-/opt/smart-home/src/identity}"
IDENTITY_PORT="${IDENTITY_PORT:-8097}"               # LAN-facing — kiosk browsers call this directly
IDENTITY_WEB_PORT="${IDENTITY_WEB_PORT:-8098}"           # LAN-facing read-only static serving (register.html/dashboard.html)

# --- Trash collection date sync (Phase 19) — off by default until WASTE_ICS_URL and
# --- CALDAV_TARGET_CALENDAR are provisioned. See trash-calendar/README.md. A oneshot,
# --- like digest-engine, not a listener — no port, nothing to publish.
ENABLE_TRASH_CALENDAR="${ENABLE_TRASH_CALENDAR:-false}"
# Where this repo's trash-calendar/ directory lives on THIS host (build context).
TRASH_CALENDAR_SRC="${TRASH_CALENDAR_SRC:-/opt/smart-home/src/trash-calendar}"

# --- Public transit "when's the next bus" voice lookup (Phase 19) — off by default
# --- until TRANSIT_TOKEN and GTFS_FEED_URL are provisioned. See transit/README.md.
# --- Published like pantry-vision/identity — homeassistant's network_mode: host
# --- can't resolve container DNS names, so its rest_command needs a real port.
ENABLE_TRANSIT="${ENABLE_TRANSIT:-false}"
# Where this repo's transit/ directory lives on THIS host (build context).
TRANSIT_SRC="${TRANSIT_SRC:-/opt/smart-home/src/transit}"
TRANSIT_PORT="${TRANSIT_PORT:-8099}"                # reachable by HA's rest_command (voice lookups)

# --- On-demand route planning (Phase 19) — a SEPARATE opt-in from ENABLE_TRANSIT
# --- above on purpose: a real OpenTripPlanner graph (OSM + GTFS) is a meaningfully
# --- bigger data/hardware commitment than the small filtered GTFS DB /departures
# --- uses. See transit/README.md's "Route planning scope" before turning this on —
# --- "Austria-wide" is a moderate commitment, "global" is a real infrastructure
# --- decision, not a flag. This script does NOT build the OTP graph for you — that's
# --- a manual, one-time (per OSM/GTFS update) step; see OpenTripPlanner's own docs.
ENABLE_TRIP_PLANNING="${ENABLE_TRIP_PLANNING:-false}"
OTP_GRAPHS_DIR="${OTP_GRAPHS_DIR:-/opt/smart-home/otp-graphs}"  # you populate this by hand, see above
# Host-side published port for OTP's own web/GraphQL API. NOT 8080 — zigbee2mqtt's
# frontend (always-on, below) already publishes 8080:8080; OTP's own container-
# internal port stays 8080 regardless (transit.env.example's OTP_URL correctly
# reaches it via container DNS as http://otp:8080), only the host-side mapping
# needed to move to avoid the two colliding on the same host.
OTP_PORT="${OTP_PORT:-8100}"

# --- Household chore distribution + reminders + camera verification (Phase 20) —
# --- off by default. Works with just ENABLE_IDENTITY on (assignment) and ntfy
# --- (reminders); the camera-check and trash-day-eve steps each individually
# --- no-op until their own env vars are set — see chores/README.md.
ENABLE_CHORES="${ENABLE_CHORES:-false}"
# Where this repo's chores/ directory lives on THIS host (build context).
CHORES_SRC="${CHORES_SRC:-/opt/smart-home/src/chores}"

# --- Music Assistant (optional, additive — docs/project-plan.md §2) ---------
# Unifies Spotify Connect + other sources behind one HA-native multi-room player.
# Runs as its OWN container (official image, not an HA add-on — this stack is a
# plain HA Container install, which has no add-on store) — configure HA's own
# "Music Assistant" integration afterwards to point at it. Does not replace or
# depend on any host's existing per-room spotifyd/librespot/Spotify-client setup
# (Phase 11.6/15/16); those keep working standalone either way. network_mode: host
# because player discovery (Chromecast/AirPlay/Sonos-style mDNS) needs it — same
# reasoning as the homeassistant service itself using host networking.
#
# VERIFY: Music Assistant's own default web UI/API port is ASSUMED to be 8095,
# not confirmed against a running instance — and 8095 is already this stack's
# PANTRY_VISION_PORT (below). Because network_mode: host bypasses Compose's own
# port-mapping/collision-checking entirely, enabling both together with an actual
# port clash would just mean one of them fails to bind, not an obvious error.
# Check Music Assistant's own docs/config for how to change its listen port
# BEFORE flipping this on if ENABLE_PANTRY_VISION is also true — the sanity check
# below only warns, it does not change either port for you.
ENABLE_MUSIC_ASSISTANT="${ENABLE_MUSIC_ASSISTANT:-false}"

# ---------------------------------------------------------------------------
# Sanity checks
# ---------------------------------------------------------------------------
if [[ $EUID -ne 0 ]]; then
  echo "Please run as root (sudo ./setup-container-host.sh)" >&2
  exit 1
fi

if ! grep -qi "debian" /etc/os-release; then
  echo "Warning: this script targets Debian/Raspberry Pi OS. Proceeding anyway..."
fi

if [[ ! -e "$ZIGBEE_USB_DEVICE" ]]; then
  echo "Warning: $ZIGBEE_USB_DEVICE does not exist yet."
  echo "  Plug in your Zigbee adapter and run 'ls -l /dev/serial/by-id/' to find the correct path,"
  echo "  then edit ZIGBEE_USB_DEVICE at the top of this script before re-running."
  echo "  Continuing anyway — the zigbee2mqtt container just won't start correctly until this is fixed."
fi

if [[ "$ENABLE_BACKUPS" == "true" && "$RESTIC_REPOSITORY" == "/mnt/backup/smart-home-restic" ]]; then
  echo "Warning: ENABLE_BACKUPS=true but RESTIC_REPOSITORY is still the placeholder path."
  echo "  Edit RESTIC_REPOSITORY at the top of this script to point at real backup storage."
fi

if [[ "$ENABLE_DIGEST_ENGINE" == "true" && ! -d "$DIGEST_ENGINE_SRC" ]]; then
  echo "Warning: ENABLE_DIGEST_ENGINE=true but $DIGEST_ENGINE_SRC does not exist."
  echo "  Copy or clone this repo's digest-engine/ directory there — it is the"
  echo "  build context for the digest-engine image — then re-run."
fi

if [[ "$ENABLE_ADMIN_CANVAS" == "true" && ! -d "$ADMIN_CANVAS_SRC" ]]; then
  echo "Warning: ENABLE_ADMIN_CANVAS=true but $ADMIN_CANVAS_SRC does not exist."
  echo "  Copy or clone this repo's admin-canvas/ directory there — it is the"
  echo "  build context for the admin-canvas image — then re-run."
fi

if [[ "$ENABLE_PANTRY_VISION" == "true" && ! -d "$PANTRY_VISION_SRC" ]]; then
  echo "Warning: ENABLE_PANTRY_VISION=true but $PANTRY_VISION_SRC does not exist."
  echo "  Copy or clone this repo's pantry-vision/ directory there — it is the"
  echo "  build context for the pantry-vision image — then re-run."
fi

if [[ "$ENABLE_IDENTITY" == "true" && ! -d "$IDENTITY_SRC" ]]; then
  echo "Warning: ENABLE_IDENTITY=true but $IDENTITY_SRC does not exist."
  echo "  Copy or clone this repo's identity/ directory there — it is the build"
  echo "  context for the identity image — then re-run."
fi

if [[ "$ENABLE_TRASH_CALENDAR" == "true" && ! -d "$TRASH_CALENDAR_SRC" ]]; then
  echo "Warning: ENABLE_TRASH_CALENDAR=true but $TRASH_CALENDAR_SRC does not exist."
  echo "  Copy or clone this repo's trash-calendar/ directory there — it is the"
  echo "  build context for the trash-calendar image — then re-run."
fi

if [[ "$ENABLE_TRANSIT" == "true" && ! -d "$TRANSIT_SRC" ]]; then
  echo "Warning: ENABLE_TRANSIT=true but $TRANSIT_SRC does not exist."
  echo "  Copy or clone this repo's transit/ directory there — it is the build"
  echo "  context for the transit image — then re-run."
fi

if [[ "$ENABLE_TRIP_PLANNING" == "true" && ! -d "$OTP_GRAPHS_DIR" ]]; then
  echo "Warning: ENABLE_TRIP_PLANNING=true but $OTP_GRAPHS_DIR does not exist or is"
  echo "  empty. OpenTripPlanner needs a built graph there before it can serve"
  echo "  anything — see transit/README.md's 'Route planning scope' section. The"
  echo "  otp container will start but /plan will fail until a graph exists."
fi

if [[ "$ENABLE_CHORES" == "true" && ! -d "$CHORES_SRC" ]]; then
  echo "Warning: ENABLE_CHORES=true but $CHORES_SRC does not exist."
  echo "  Copy or clone this repo's chores/ directory there — it is the build"
  echo "  context for the chores image — then re-run."
fi

if [[ "$ENABLE_MUSIC_ASSISTANT" == "true" && "$ENABLE_PANTRY_VISION" == "true" ]]; then
  echo "Warning: ENABLE_MUSIC_ASSISTANT=true and ENABLE_PANTRY_VISION=true together."
  echo "  Music Assistant's own default port is ASSUMED to be $PANTRY_VISION_PORT (unverified,"
  echo "  see ENABLE_MUSIC_ASSISTANT's own comment above) — the SAME as PANTRY_VISION_PORT."
  echo "  Because Music Assistant runs with network_mode: host, a real clash here is NOT"
  echo "  caught by Docker Compose the way a normal port mapping would be. Check Music"
  echo "  Assistant's own config for its listen port before starting the stack."
fi

if [[ "$ENABLE_WHATSAPP_INGEST" == "true" ]]; then
  echo "WARNING: WhatsApp ingestion is enabled."
  echo "  There is no officially sanctioned way to read WhatsApp programmatically."
  echo "  whatsapp-bridge runs a real headful Chromium logged in as a linked device,"
  echo "  which lowers but does not remove the ban risk (~2-8 weeks, historically)."
  echo "  Use a secondary/non-critical number. See digest-engine/README.md."
fi

if [[ "$ENABLE_GALLERY_SMB" == "true" && -z "$GALLERY_SMB_PASSWORD" ]]; then
  echo "Warning: ENABLE_GALLERY_SMB=true but GALLERY_SMB_PASSWORD is still empty."
  echo "  Set a real password at the top of this script — the same value then has to be"
  echo "  typed into /etc/thinclient-agent/gallery-credentials on every thin client that"
  echo "  should mount this share, since nothing here can push it to those machines."
fi

HOST_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
HOST_IP="${HOST_IP:-<host-ip>}"

echo "=== Smart Home Container Host Setup ==="
echo "Base directory: $BASE_DIR"
echo "Timezone: $TIMEZONE"
echo "Host IP (detected): $HOST_IP"
echo "Zigbee adapter (USB): ${ZIGBEE_USB_DEVICE}"
echo

# ---------------------------------------------------------------------------
# 1. System update + prerequisites
# ---------------------------------------------------------------------------
echo "--- Updating system and installing prerequisites ---"
apt-get update
apt-get upgrade -y
apt-get install -y \
  ca-certificates \
  curl \
  gnupg \
  lsb-release \
  jq \
  openssl

if [[ "$ENABLE_BACKUPS" == "true" ]]; then
  apt-get install -y restic
fi

# ---------------------------------------------------------------------------
# 2. Install Docker Engine + Compose plugin (official Docker repo)
# ---------------------------------------------------------------------------
if ! command -v docker &> /dev/null; then
  echo "--- Installing Docker Engine ---"
  install -m 0755 -d /etc/apt/keyrings
  curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
  chmod a+r /etc/apt/keyrings/docker.asc

  ARCH="$(dpkg --print-architecture)"
  CODENAME="$(. /etc/os-release && echo "$VERSION_CODENAME")"
  echo \
    "deb [arch=${ARCH} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${CODENAME} stable" \
    > /etc/apt/sources.list.d/docker.list

  apt-get update
  apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

  # Allow the invoking (non-root) user to run docker without sudo, if applicable
  if [[ -n "${SUDO_USER:-}" ]]; then
    usermod -aG docker "$SUDO_USER"
    echo "Added $SUDO_USER to the docker group. Log out/in for it to take effect."
  fi
else
  echo "--- Docker already installed, skipping ---"
fi

# ---------------------------------------------------------------------------
# 3. Directory structure
# ---------------------------------------------------------------------------
echo "--- Creating directory structure under $BASE_DIR ---"
mkdir -p "$BASE_DIR"/{homeassistant,mosquitto/config,mosquitto/data,mosquitto/log,zigbee2mqtt/data,frigate/config,frigate/media,grocy/config,grocy/data}
if [[ "$ENABLE_MEALIE" == "true" ]]; then
  mkdir -p "$BASE_DIR"/mealie/data
fi
if [[ "$ENABLE_NODERED" == "true" ]]; then
  mkdir -p "$BASE_DIR"/nodered/data
fi
if [[ "$ENABLE_NETDATA" == "true" ]]; then
  mkdir -p "$BASE_DIR"/netdata/{config,lib,cache}
fi
if [[ "$ENABLE_HOMEPAGE" == "true" ]]; then
  mkdir -p "$BASE_DIR"/homepage/config
fi
if [[ "$ENABLE_NTFY" == "true" ]]; then
  mkdir -p "$BASE_DIR"/ntfy/{data,config}
fi
if [[ "$ENABLE_PORTAINER" == "true" ]]; then
  mkdir -p "$BASE_DIR"/portainer/data
fi
if [[ "$ENABLE_GALLERY_SMB" == "true" ]]; then
  # The share root itself. Nothing seeds it with photos — that's a human copying
  # files in; an empty share is a valid, harmless state (the thin-client slideshow
  # skips idle-gallery mode with nothing to show, same as an unreachable share).
  mkdir -p "$BASE_DIR"/gallery
fi
if [[ "$ENABLE_PHOTOS_WEB" == "true" ]]; then
  # Immich's own library and database. Separate from the gallery share, which it only
  # ever reads: two writers to one photo tree with different ideas of the layout is how
  # a collection gets quietly reorganised.
  mkdir -p "$BASE_DIR"/photos/{db,library}
fi
if [[ "$ENABLE_WORKSHOP" == "true" ]]; then
  # data/ holds the two SQLite files (work, and never-pruned knowledge); workspace/ is
  # the writable SMB share the assistant files artefacts into.
  mkdir -p "$BASE_DIR"/workshop/{data,workspace}
  if [[ ! -f "$BASE_DIR/workshop/workshop.env" ]]; then
    cp "$WORKSHOP_SRC/workshop.env.example" "$BASE_DIR/workshop/workshop.env"
    chmod 600 "$BASE_DIR/workshop/workshop.env"
    echo "  Seeded $BASE_DIR/workshop/workshop.env from the template — fill in a real"
    echo "  WORKSHOP_TOKEN (openssl rand -hex 32). CheckMK, the firewall list and Gitea"
    echo "  are all optional; each is refused with a clear message while unset."
  fi
fi
if [[ "$ENABLE_DIGEST_ENGINE" == "true" ]]; then
  mkdir -p "$BASE_DIR"/digest/{output,data}
  if [[ "$ENABLE_WHATSAPP_INGEST" == "true" ]]; then
    mkdir -p "$BASE_DIR"/digest/data/whatsapp-bridge
  fi
  # The real credentials file. Seed it from the committed template on first run;
  # 600 like backup.env, and never committed (repo .gitignore covers *.env).
  if [[ ! -f "$BASE_DIR/digest/digest-engine.env" ]]; then
    cp "$DIGEST_ENGINE_SRC/digest-engine.env.example" "$BASE_DIR/digest/digest-engine.env"
    chmod 600 "$BASE_DIR/digest/digest-engine.env"
    echo "  Seeded $BASE_DIR/digest/digest-engine.env from the template — fill in real values."
    echo "  IDENTITY_TOKEN there is the same shared secret chores.env uses; without it the"
    echo "  run can't read who wants which digest and generates all four sections."
  fi
  # The OPNsense IDS config. An image build writes this from CoreSystemConfig.json's
  # opnsense block; this branch is the hand-install path, where the template is the
  # only thing there is. Never overwritten — a real config outranks a template.
  if [[ ! -f "$BASE_DIR/digest/data/IDSconf.json" && -f "$DIGEST_ENGINE_SRC/IDSconf.json.example" ]]; then
    cp "$DIGEST_ENGINE_SRC/IDSconf.json.example" "$BASE_DIR/digest/data/IDSconf.json"
    chmod 600 "$BASE_DIR/digest/data/IDSconf.json"
    echo "  Seeded $BASE_DIR/digest/data/IDSconf.json from the template — fill in base_url,"
    echo "  api_key and api_secret (OPNsense: System -> Access -> Users -> API keys) and set"
    echo "  ENABLE_OPNSENSE_IDS_INGEST=true if you want the network digest."
  fi
fi
if [[ "$ENABLE_ADMIN_CANVAS" == "true" ]]; then
  mkdir -p "$BASE_DIR"/admin-canvas/output/media
  # Same handling as digest-engine.env above: seed from the committed template on
  # first run, 600, never committed (repo .gitignore covers *.env).
  if [[ ! -f "$BASE_DIR/admin-canvas/admin-canvas.env" ]]; then
    cp "$ADMIN_CANVAS_SRC/admin-canvas.env.example" "$BASE_DIR/admin-canvas/admin-canvas.env"
    chmod 600 "$BASE_DIR/admin-canvas/admin-canvas.env"
    echo "  Seeded $BASE_DIR/admin-canvas/admin-canvas.env from the template — fill in a real ADMIN_CANVAS_TOKEN."
  fi
fi
if [[ "$ENABLE_PANTRY_VISION" == "true" ]]; then
  # data/ holds ONE file: doorway.py's hint database (what an appliance camera thinks
  # it saw at a door, with a timestamp). Everything that is actually inventory still
  # lives in Grocy — see pantry-vision/doorway.py on why an observation and a stock
  # row are deliberately not kept in the same place. Losing this directory costs the
  # household its recent sightings and nothing else.
  mkdir -p "$BASE_DIR"/pantry-vision/data
  # Same handling as admin-canvas.env above: seed from the committed template on
  # first run, 600, never committed (repo .gitignore covers *.env).
  if [[ ! -f "$BASE_DIR/pantry-vision/pantry-vision.env" ]]; then
    cp "$PANTRY_VISION_SRC/pantry-vision.env.example" "$BASE_DIR/pantry-vision/pantry-vision.env"
    chmod 600 "$BASE_DIR/pantry-vision/pantry-vision.env"
    echo "  Seeded $BASE_DIR/pantry-vision/pantry-vision.env from the template — fill in a real"
    echo "  PANTRY_VISION_TOKEN and GROCY_API_KEY (Grocy's own UI: Settings -> Manage API keys)."
  fi
fi
if [[ "$ENABLE_IDENTITY" == "true" ]]; then
  # pantry-vision keeps only doorway hints locally (everything that is inventory is
  # Grocy's); identity is different in kind — it owns its own SQLite DB and the
  # registration photos, and neither is reconstructible from anywhere else.
  mkdir -p "$BASE_DIR"/identity/data/photos
  if [[ ! -f "$BASE_DIR/identity/identity.env" ]]; then
    cp "$IDENTITY_SRC/identity.env.example" "$BASE_DIR/identity/identity.env"
    chmod 600 "$BASE_DIR/identity/identity.env"
    echo "  Seeded $BASE_DIR/identity/identity.env from the template — fill in a real"
    echo "  IDENTITY_TOKEN, HA_TOKEN (HA's own UI: profile -> Security -> Long-Lived"
    echo "  Access Tokens), and TRUSTED_ENTITY_PREFIXES (Developer Tools -> States)."
  fi
fi
if [[ "$ENABLE_TRASH_CALENDAR" == "true" ]]; then
  mkdir -p "$BASE_DIR"/trash-calendar
  if [[ ! -f "$BASE_DIR/trash-calendar/trash-calendar.env" ]]; then
    cp "$TRASH_CALENDAR_SRC/trash-calendar.env.example" "$BASE_DIR/trash-calendar/trash-calendar.env"
    chmod 600 "$BASE_DIR/trash-calendar/trash-calendar.env"
    echo "  Seeded $BASE_DIR/trash-calendar/trash-calendar.env from the template — fill in"
    echo "  a real WASTE_ICS_URL and CALDAV_TARGET_CALENDAR (same CALDAV_* credentials as"
    echo "  digest-engine's own ingest/caldav.py, reused — see trash-calendar/README.md)."
  fi
fi
if [[ "$ENABLE_TRANSIT" == "true" ]]; then
  mkdir -p "$BASE_DIR"/transit/data
  if [[ ! -f "$BASE_DIR/transit/transit.env" ]]; then
    cp "$TRANSIT_SRC/transit.env.example" "$BASE_DIR/transit/transit.env"
    chmod 600 "$BASE_DIR/transit/transit.env"
    echo "  Seeded $BASE_DIR/transit/transit.env from the template — fill in a real"
    echo "  TRANSIT_TOKEN and GTFS_FEED_URL (see transit/README.md for where to get it)."
  fi
fi
if [[ "$ENABLE_TRIP_PLANNING" == "true" ]]; then
  mkdir -p "$OTP_GRAPHS_DIR"
fi
if [[ "$ENABLE_CHORES" == "true" ]]; then
  mkdir -p "$BASE_DIR"/chores/data
  if [[ ! -f "$BASE_DIR/chores/chores.env" ]]; then
    cp "$CHORES_SRC/chores.env.example" "$BASE_DIR/chores/chores.env"
    chmod 600 "$BASE_DIR/chores/chores.env"
    echo "  Seeded $BASE_DIR/chores/chores.env from the template — fill in IDENTITY_TOKEN"
    echo "  and ntfy settings at minimum; camera checks and trash-day-eve are each"
    echo "  optional, see chores/README.md."
  fi
fi
if [[ "$ENABLE_MUSIC_ASSISTANT" == "true" ]]; then
  mkdir -p "$BASE_DIR"/music-assistant/data
fi

# ---------------------------------------------------------------------------
# 4. Mosquitto config
# ---------------------------------------------------------------------------
echo "--- Writing Mosquitto config ---"
cat > "$BASE_DIR/mosquitto/config/mosquitto.conf" <<'EOF'
# Basic internal-network broker config.
# This trusts anything on your Docker/LAN network. If Mosquitto will ever be
# reachable beyond your trusted LAN, add password_file auth before exposing it.
listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
EOF

# ---------------------------------------------------------------------------
# 5. Zigbee2MQTT config (pre-seeded for your USB CC2652P dongle)
# ---------------------------------------------------------------------------
echo "--- Writing Zigbee2MQTT config ---"
cat > "$BASE_DIR/zigbee2mqtt/data/configuration.yaml" <<EOF
homeassistant: true
permit_join: false
mqtt:
  base_topic: zigbee2mqtt
  server: 'mqtt://mosquitto:1883'
serial:
  port: '/dev/ttyACM0'
  adapter: zstack
advanced:
  network_key: GENERATE
frontend:
  port: 8080
EOF
echo "  NOTE: adapter set to 'zstack' for your CC2652P (Haozee/Sonoff Dongle-P style) USB dongle."
echo "  If Zigbee2MQTT fails to connect, it may need CC2652P coordinator firmware flashed first"
echo "  (generic-branded gateways sometimes ship with non-coordinator firmware)."

# ---------------------------------------------------------------------------
# 6. Frigate config (placeholder — edit before first real use)
# ---------------------------------------------------------------------------
echo "--- Writing Frigate placeholder config ---"
cat > "$BASE_DIR/frigate/config/config.yml" <<'EOF'
mqtt:
  host: mosquitto
  port: 1883

# Face recognition (Frigate 0.16+)
face_recognition:
  enabled: true

cameras:
  peephole:
    ffmpeg:
      inputs:
        - path: rtsp://USERNAME:PASSWORD@CAMERA_IP:554/STREAM_PATH
          roles:
            - detect
            - record
    detect:
      width: 1280
      height: 720
      fps: 5
    record:
      enabled: true

# Uncomment and adjust if using Intel QuickSync/OpenVINO hardware acceleration:
# ffmpeg:
#   hwaccel_args: preset-vaapi
# detectors:
#   ov:
#     type: openvino
#     device: GPU
EOF
echo "  NOTE: edit $BASE_DIR/frigate/config/config.yml with your real camera RTSP URL before starting Frigate."

# ---------------------------------------------------------------------------
# 7. Homepage dashboard config (single landing page over the whole stack)
# ---------------------------------------------------------------------------
if [[ "$ENABLE_HOMEPAGE" == "true" ]]; then
  echo "--- Writing Homepage config ---"
  cat > "$BASE_DIR/homepage/config/settings.yaml" <<'EOF'
title: Smart Home
theme: dark
color: slate
headerStyle: clean
EOF

  cat > "$BASE_DIR/homepage/config/widgets.yaml" <<'EOF'
- resources:
    cpu: true
    memory: true
    disk: /
EOF

  cat > "$BASE_DIR/homepage/config/bookmarks.yaml" <<'EOF'
[]
EOF

  cat > "$BASE_DIR/homepage/config/services.yaml" <<EOF
- Home:
    - Home Assistant:
        href: http://${HOST_IP}:8123
        description: Home automation hub
    - Node-RED:
        href: http://${HOST_IP}:1880
        description: Automation flows
- Devices:
    - Zigbee2MQTT:
        href: http://${HOST_IP}:8080
        description: Zigbee network
    - Frigate:
        href: http://${HOST_IP}:5000
        description: NVR / face recognition
- Household:
    - Grocy:
        href: http://${HOST_IP}:9283
        description: Inventory & shopping list
- System:
    - Portainer:
        href: http://${HOST_IP}:9000
        description: Container management
    - Netdata:
        href: http://${HOST_IP}:19999
        description: Host/container monitoring
    - ntfy:
        href: http://${HOST_IP}:8090
        description: Push notifications
EOF
fi

# ---------------------------------------------------------------------------
# 8. docker-compose.yml
# ---------------------------------------------------------------------------
echo "--- Writing docker-compose.yml ---"

FRIGATE_DEVICES=""
if [[ "$ENABLE_INTEL_HWACCEL" == "true" ]]; then
  FRIGATE_DEVICES="    devices:
      - /dev/dri:/dev/dri"
fi

MEALIE_BLOCK=""
if [[ "$ENABLE_MEALIE" == "true" ]]; then
  MEALIE_BLOCK="
  mealie:
    image: ghcr.io/mealie-recipes/mealie:latest
    container_name: mealie
    restart: unless-stopped
    ports:
      - \"9925:9000\"
    volumes:
      - ${BASE_DIR}/mealie/data:/app/data
    environment:
      - TZ=${TIMEZONE}
      - ALLOW_SIGNUP=false
"
fi

NODERED_BLOCK=""
if [[ "$ENABLE_NODERED" == "true" ]]; then
  NODERED_BLOCK="
  nodered:
    image: nodered/node-red:latest
    container_name: nodered
    restart: unless-stopped
    depends_on:
      - mosquitto
    ports:
      - \"1880:1880\"
    volumes:
      - ${BASE_DIR}/nodered/data:/data
      - /etc/localtime:/etc/localtime:ro
    environment:
      - TZ=${TIMEZONE}
"
fi

NETDATA_BLOCK=""
if [[ "$ENABLE_NETDATA" == "true" ]]; then
  NETDATA_BLOCK="
  netdata:
    image: netdata/netdata:stable
    container_name: netdata
    restart: unless-stopped
    hostname: smart-home-host
    network_mode: host
    pid: host
    cap_add:
      - SYS_PTRACE
      - SYS_ADMIN
    security_opt:
      - apparmor:unconfined
    volumes:
      - ${BASE_DIR}/netdata/config:/etc/netdata
      - ${BASE_DIR}/netdata/lib:/var/lib/netdata
      - ${BASE_DIR}/netdata/cache:/var/cache/netdata
      - /etc/passwd:/host/etc/passwd:ro
      - /etc/group:/host/etc/group:ro
      - /etc/os-release:/host/etc/os-release:ro
      - /var/log:/host/var/log:ro
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
"
fi

HOMEPAGE_BLOCK=""
if [[ "$ENABLE_HOMEPAGE" == "true" ]]; then
  HOMEPAGE_BLOCK="
  homepage:
    image: ghcr.io/gethomepage/homepage:latest
    container_name: homepage
    restart: unless-stopped
    ports:
      - \"3000:3000\"
    volumes:
      - ${BASE_DIR}/homepage/config:/app/config
    environment:
      - TZ=${TIMEZONE}
"
fi

NTFY_BLOCK=""
if [[ "$ENABLE_NTFY" == "true" ]]; then
  NTFY_BLOCK="
  ntfy:
    image: binwiederhier/ntfy:latest
    container_name: ntfy
    restart: unless-stopped
    command: serve
    ports:
      - \"8090:80\"
    volumes:
      - ${BASE_DIR}/ntfy/data:/var/lib/ntfy
      - ${BASE_DIR}/ntfy/config:/etc/ntfy
    environment:
      - TZ=${TIMEZONE}
"
fi

PORTAINER_BLOCK=""
if [[ "$ENABLE_PORTAINER" == "true" ]]; then
  PORTAINER_BLOCK="
  portainer:
    image: portainer/portainer-ce:lts
    container_name: portainer
    restart: unless-stopped
    command: -H unix:///var/run/docker.sock
    ports:
      - \"9000:9000\"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${BASE_DIR}/portainer/data:/data
"
fi

# The workshop workspace is a SECOND SHARE ON THE SAME SAMBA CONTAINER, not a second
# container: Samba serves many shares on one port, and a second container would fight
# this one for 445 — the same class of collision already recorded as open decision #31.
# It is the one WRITABLE share in this stack (the gallery is read only = yes), so it
# gets its own volume and its own account; see workshop/README.md for why that matters
# when the thing writing into it is an LLM.
WORKSHOP_SHARE_ENV=""
WORKSHOP_SHARE_VOLUME=""
if [[ "$ENABLE_WORKSHOP" == "true" && "$ENABLE_GALLERY_SMB" == "true" ]]; then
  WORKSHOP_SHARE_ENV="
      - ACCOUNT_${WORKSHOP_SMB_USERNAME}=${WORKSHOP_SMB_PASSWORD}
      - SAMBA_VOLUME_CONFIG_workshop=path = /shares/workshop; valid users = ${WORKSHOP_SMB_USERNAME}; guest ok = no; read only = no; browseable = yes"
  WORKSHOP_SHARE_VOLUME="
      - ${BASE_DIR}/workshop/workspace:/shares/workshop"
elif [[ "$ENABLE_WORKSHOP" == "true" ]]; then
  echo "  NOTE: ENABLE_WORKSHOP is on but ENABLE_GALLERY_SMB is off, so there is no Samba"
  echo "  container to hang the workspace share on. The workshop service works fine —"
  echo "  its notebook is in SQLite — but artefacts will only be reachable through the"
  echo "  container filesystem until you enable the SMB container too."
fi

# Immich: search/albums/faces over the SAME directory the SMB share exports. Two views
# of one pile of files, which is the point — the share is right for bulk copy and for
# keeping the photos openable with no software at all, and hopeless for finding one.
# It is mounted READ-ONLY here on purpose: Immich's own upload path would put files
# where its database expects them rather than where the share's layout puts them, and
# two writers to one tree with different ideas of the layout is how a photo collection
# gets quietly reorganised. Import from the share, write nothing back.
PHOTOS_WEB_BLOCK=""
if [[ "$ENABLE_PHOTOS_WEB" == "true" ]]; then
  if [[ -z "$PHOTOS_DB_PASSWORD" ]]; then
    echo "ERROR: ENABLE_PHOTOS_WEB=true but PHOTOS_DB_PASSWORD is empty." >&2
    exit 1
  fi
  PHOTOS_WEB_BLOCK="
  immich-db:
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
    container_name: immich-db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=immich
      - POSTGRES_PASSWORD=${PHOTOS_DB_PASSWORD}
      - POSTGRES_DB=immich
      - TZ=${TIMEZONE}
    volumes:
      - ${BASE_DIR}/photos/db:/var/lib/postgresql/data

  immich-redis:
    image: docker.io/valkey/valkey:8-bookworm
    container_name: immich-redis
    restart: unless-stopped

  immich-server:
    image: ghcr.io/immich-app/immich-server:release
    container_name: immich-server
    restart: unless-stopped
    depends_on:
      - immich-db
      - immich-redis
    ports:
      - \"${PHOTOS_WEB_PORT}:2283\"
    environment:
      - DB_HOSTNAME=immich-db
      - DB_USERNAME=immich
      - DB_PASSWORD=${PHOTOS_DB_PASSWORD}
      - DB_DATABASE_NAME=immich
      - REDIS_HOSTNAME=immich-redis
      - TZ=${TIMEZONE}
    volumes:
      - ${BASE_DIR}/photos/library:/usr/src/app/upload
      # The gallery, read-only. Add it in Immich as an External Library.
      - ${BASE_DIR}/gallery:/mnt/gallery:ro
"
fi

# workshop: the project notebook, the knowledge store and the health poller.
# /data holds two SQLite files (work, and never-pruned knowledge); /workspace is the
# writable SMB share. See workshop/README.md.
WORKSHOP_BLOCK=""
WORKSHOP_WEB_BLOCK=""
if [[ "$ENABLE_WORKSHOP" == "true" ]]; then
  WORKSHOP_BLOCK="
  workshop:
    build: ${WORKSHOP_SRC}
    image: smart-home/workshop:local
    container_name: workshop
    restart: unless-stopped
    ports:
      - \"${WORKSHOP_PORT}:${WORKSHOP_PORT}\"
    volumes:
      - ${BASE_DIR}/workshop/data:/data
      - ${BASE_DIR}/workshop/workspace:/workspace
    env_file:
      - ${BASE_DIR}/workshop/workshop.env
    environment:
      - WORKSHOP_PORT=${WORKSHOP_PORT}
      - TZ=${TIMEZONE}
"
  WORKSHOP_WEB_BLOCK="
  workshop-web:
    image: nginx:alpine
    container_name: workshop-web
    restart: unless-stopped
    ports:
      - \"${WORKSHOP_WEB_PORT}:80\"
    volumes:
      - ${WORKSHOP_SRC}/frontend:/usr/share/nginx/html:ro
    environment:
      - TZ=${TIMEZONE}
"
fi

GALLERY_SMB_BLOCK=""
if [[ "$ENABLE_GALLERY_SMB" == "true" ]]; then
  # servercontainers/samba over dperson/samba: dperson's image has gone without
  # maintainer updates long enough that forks exist specifically to patch CVEs in
  # it; servercontainers is the actively maintained one as of this writing.
  # ACCOUNT_gallery / SAMBA_VOLUME_CONFIG_gallery is that image's env-driven
  # config surface — no config file to template, just these two variables.
  GALLERY_SMB_BLOCK="
  gallery-smb:
    image: ghcr.io/servercontainers/samba:latest
    container_name: gallery-smb
    restart: unless-stopped
    ports:
      - \"445:445\"
    environment:
      - TZ=${TIMEZONE}
      - ACCOUNT_${GALLERY_SMB_USERNAME}=${GALLERY_SMB_PASSWORD}
      - SAMBA_VOLUME_CONFIG_gallery=path = /shares/gallery; valid users = ${GALLERY_SMB_USERNAME}; guest ok = no; read only = yes; browseable = yes${WORKSHOP_SHARE_ENV}
    volumes:
      - ${BASE_DIR}/gallery:/shares/gallery${WORKSHOP_SHARE_VOLUME}
    cap_add:
      - NET_ADMIN
"
fi

# digest-engine and whatsapp-bridge are the FIRST locally-built images in this
# stack — everything else above pulls a prebuilt registry image. That means
# this host needs the digest-engine/ source tree present at DIGEST_ENGINE_SRC,
# and a `docker compose build` after any code change there.
DIGEST_ENGINE_BLOCK=""
DIGEST_WEB_BLOCK=""
WHATSAPP_BRIDGE_BLOCK=""
if [[ "$ENABLE_DIGEST_ENGINE" == "true" ]]; then
  # profiles: keeps this out of `docker compose up -d` — it's a oneshot driven
  # by smart-home-digest.timer below. `docker compose run --rm digest-engine`
  # still works regardless of profile.
  DIGEST_ENGINE_BLOCK="
  digest-engine:
    build: ${DIGEST_ENGINE_SRC}
    image: smart-home/digest-engine:local
    container_name: digest-engine
    profiles:
      - oneshot
    restart: \"no\"
    env_file:
      - ${BASE_DIR}/digest/digest-engine.env
    volumes:
      - ${BASE_DIR}/digest/output:/output
      - ${BASE_DIR}/digest/data:/data
      - /etc/localtime:/etc/localtime:ro
    environment:
      - TZ=${TIMEZONE}
"

  # Static, read-only serving of the rendered digest to the thin client's kiosk
  # Firefox workspace and an HA Lovelace iframe card.
  DIGEST_WEB_BLOCK="
  digest-web:
    image: nginx:alpine
    container_name: digest-web
    restart: unless-stopped
    ports:
      - \"${DIGEST_WEB_PORT}:80\"
    volumes:
      - ${DIGEST_ENGINE_SRC}/render/templates/compact.html:/usr/share/nginx/html/compact.html:ro
      - ${DIGEST_ENGINE_SRC}/render/templates/full.html:/usr/share/nginx/html/full.html:ro
      - ${DIGEST_ENGINE_SRC}/render/digest-canvas-sdk:/usr/share/nginx/html/digest-canvas-sdk:ro
      - ${BASE_DIR}/digest/output:/usr/share/nginx/html/output:ro
    environment:
      - TZ=${TIMEZONE}
"
fi

# admin-canvas (Phase 13) — the on-demand sys-admin-llm display surface. Same
# write/read split as digest-engine/digest-web above, except the write side is a
# small always-on listener (restart: unless-stopped) rather than a oneshot, since
# content here arrives whenever HA calls it rather than on a timer. Deliberately
# NO `ports:` entry on admin-canvas itself — it must only ever be reachable from
# other containers on this compose network (i.e. homeassistant), the same trust
# boundary mosquitto<->homeassistant already share. See admin-canvas/README.md.
ADMIN_CANVAS_BLOCK=""
ADMIN_WEB_BLOCK=""
if [[ "$ENABLE_ADMIN_CANVAS" == "true" ]]; then
  ADMIN_CANVAS_BLOCK="
  admin-canvas:
    build: ${ADMIN_CANVAS_SRC}
    image: smart-home/admin-canvas:local
    container_name: admin-canvas
    restart: unless-stopped
    env_file:
      - ${BASE_DIR}/admin-canvas/admin-canvas.env
    volumes:
      - ${BASE_DIR}/admin-canvas/output:/output
      - /etc/localtime:/etc/localtime:ro
    environment:
      - ADMIN_CANVAS_PORT=${ADMIN_CANVAS_PORT}
      - TZ=${TIMEZONE}
"

  ADMIN_WEB_BLOCK="
  admin-web:
    image: nginx:alpine
    container_name: admin-web
    restart: unless-stopped
    ports:
      - \"${ADMIN_WEB_PORT}:80\"
    volumes:
      - ${ADMIN_CANVAS_SRC}/render/templates/canvas.html:/usr/share/nginx/html/canvas.html:ro
      - ${ADMIN_CANVAS_SRC}/render/canvas-sdk:/usr/share/nginx/html/canvas-sdk:ro
      - ${BASE_DIR}/admin-canvas/output:/usr/share/nginx/html/output:ro
    environment:
      - TZ=${TIMEZONE}
"
fi

# pantry-vision (Phase 17) — the kitchen display's camera-cataloguing backend. Same
# build/write-service shape as admin-canvas above, but see this block's own comment
# where it differs (a published port, since a separate physical device calls it
# directly) and pantry-vision/README.md for the full reasoning. `depends_on: grocy`
# is a startup-order hint only — pantry-vision's own outbound calls already retry per
# request, so a slow-starting Grocy delays the first real call, not the container.
PANTRY_VISION_BLOCK=""
PANTRY_WEB_BLOCK=""
if [[ "$ENABLE_PANTRY_VISION" == "true" ]]; then
  PANTRY_VISION_BLOCK="
  pantry-vision:
    build: ${PANTRY_VISION_SRC}
    image: smart-home/pantry-vision:local
    container_name: pantry-vision
    restart: unless-stopped
    depends_on:
      - grocy
    ports:
      - \"${PANTRY_VISION_PORT}:${PANTRY_VISION_PORT}\"
    volumes:
      - ${BASE_DIR}/pantry-vision/data:/data
    env_file:
      - ${BASE_DIR}/pantry-vision/pantry-vision.env
    environment:
      - PANTRY_VISION_PORT=${PANTRY_VISION_PORT}
      - TZ=${TIMEZONE}
"

  PANTRY_WEB_BLOCK="
  pantry-web:
    image: nginx:alpine
    container_name: pantry-web
    restart: unless-stopped
    ports:
      - \"${PANTRY_WEB_PORT}:80\"
    volumes:
      - ${PANTRY_VISION_SRC}/frontend:/usr/share/nginx/html:ro
    environment:
      - TZ=${TIMEZONE}
"
fi

# identity (Phase 6) — the person <-> BLE-identifier registry. Same published-port
# reasoning as pantry-vision above (hosts/kitchen-display's and hosts/door-panel's
# kiosk browsers call it directly), plus its own SQLite DB + registration-photo
# volume, since unlike pantry-vision it owns its own state instead of deferring to
# Grocy. `depends_on: homeassistant` is a startup-order hint only — it does NOT make
# Home Assistant reachable by container name, since homeassistant runs with
# `network_mode: host` and is off this compose network entirely (same situation as
# Node-RED's own HA access below); identity.env's HA_URL has to be the host's real
# LAN IP, not "homeassistant".
IDENTITY_BLOCK=""
PROXY_BLOCK=""
if [[ "$ENABLE_PROXY" == "true" ]]; then
  if [[ -z "$PROXY_HOSTNAME" ]]; then
    echo "ENABLE_PROXY is true but PROXY_HOSTNAME is empty — the certificate needs a name." >&2
    exit 1
  fi
  mkdir -p "$BASE_DIR"/proxy/{config,data}
  # The Caddyfile is generated from CoreSystemConfig.json by the ISO builder. When this
  # script is run by hand, a minimal equivalent is written here so the proxy still comes
  # up — same routes, same reasoning, just without the config file to derive from.
  if [[ ! -f "$BASE_DIR/proxy/config/Caddyfile" ]]; then
    PROXY_TLS_LINE="    tls internal"
    [[ "$PROXY_TLS" == "custom" ]] && PROXY_TLS_LINE="    tls /etc/caddy/certs/cert.pem /etc/caddy/certs/key.pem"
    cat > "$BASE_DIR/proxy/config/Caddyfile" <<PROXYEOF
{
    admin off
}

http://${PROXY_HOSTNAME} {
    redir https://{host}{uri} permanent
}

https://${PROXY_HOSTNAME} {
${PROXY_TLS_LINE}

    handle_path /api/identity/* {
        reverse_proxy identity:${IDENTITY_PORT}
    }
    handle_path /api/pantry/* {
        reverse_proxy pantry-vision:${PANTRY_VISION_PORT}
    }
    handle {
        reverse_proxy identity-web:80
    }

    header {
        Referrer-Policy "no-referrer"
        Cache-Control "no-store"
        X-Content-Type-Options "nosniff"
    }
    log {
        output stderr
        format console
    }
}
PROXYEOF
    echo "  Wrote a default $BASE_DIR/proxy/config/Caddyfile"
  fi

  PROXY_CERT_MOUNT=""
  if [[ "$PROXY_TLS" == "custom" ]]; then
    PROXY_CERT_MOUNT="
      - ${PROXY_CERT_FILE}:/etc/caddy/certs/cert.pem:ro
      - ${PROXY_KEY_FILE}:/etc/caddy/certs/key.pem:ro"
  fi

  PROXY_BLOCK="
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - \"${PROXY_HTTP_PORT}:80\"
      - \"${PROXY_HTTPS_PORT}:443\"
    volumes:
      - ${BASE_DIR}/proxy/config/Caddyfile:/etc/caddy/Caddyfile:ro
      # Caddy's own CA and issued certs live here. Persisting it matters: a fresh
      # volume means a NEW internal CA, so every client that trusted the old root
      # would start warning again.
      - ${BASE_DIR}/proxy/data:/data${PROXY_CERT_MOUNT}
    environment:
      - TZ=${TIMEZONE}
"
fi

IDENTITY_WEB_BLOCK=""
if [[ "$ENABLE_IDENTITY" == "true" ]]; then
  IDENTITY_BLOCK="
  identity:
    build: ${IDENTITY_SRC}
    image: smart-home/identity:local
    container_name: identity
    restart: unless-stopped
    depends_on:
      - homeassistant
      - mosquitto
    ports:
      - \"${IDENTITY_PORT}:${IDENTITY_PORT}\"
    env_file:
      - ${BASE_DIR}/identity/identity.env
    volumes:
      - ${BASE_DIR}/identity/data:/data
    environment:
      - IDENTITY_PORT=${IDENTITY_PORT}
      - TZ=${TIMEZONE}
"

  IDENTITY_WEB_BLOCK="
  identity-web:
    image: nginx:alpine
    container_name: identity-web
    restart: unless-stopped
    ports:
      - \"${IDENTITY_WEB_PORT}:80\"
    volumes:
      - ${IDENTITY_SRC}/frontend:/usr/share/nginx/html:ro
    environment:
      - TZ=${TIMEZONE}
"
fi

# trash-calendar (Phase 19) — oneshot, driven by smart-home-trash-calendar.timer
# below, same profiles:[oneshot] shape as digest-engine above.
TRASH_CALENDAR_BLOCK=""
if [[ "$ENABLE_TRASH_CALENDAR" == "true" ]]; then
  TRASH_CALENDAR_BLOCK="
  trash-calendar:
    build: ${TRASH_CALENDAR_SRC}
    image: smart-home/trash-calendar:local
    container_name: trash-calendar
    profiles:
      - oneshot
    restart: \"no\"
    env_file:
      - ${BASE_DIR}/trash-calendar/trash-calendar.env
    environment:
      - TZ=${TIMEZONE}
"
fi

# transit (Phase 19) — /departures is always-on (the server container below);
# /transit-sync (weekly GTFS refresh) is a oneshot, same profiles:[oneshot] shape as
# digest-engine/trash-calendar above, sharing the server's own /data volume so the
# refreshed DB is immediately visible to it.
TRANSIT_BLOCK=""
TRANSIT_SYNC_BLOCK=""
OTP_BLOCK=""
if [[ "$ENABLE_TRANSIT" == "true" ]]; then
  TRANSIT_BLOCK="
  transit:
    build: ${TRANSIT_SRC}
    image: smart-home/transit:local
    container_name: transit
    restart: unless-stopped
    ports:
      - \"${TRANSIT_PORT}:${TRANSIT_PORT}\"
    env_file:
      - ${BASE_DIR}/transit/transit.env
    volumes:
      - ${BASE_DIR}/transit/data:/data
    environment:
      - TRANSIT_PORT=${TRANSIT_PORT}
      - TZ=${TIMEZONE}
"

  TRANSIT_SYNC_BLOCK="
  transit-sync:
    build: ${TRANSIT_SRC}
    image: smart-home/transit:local
    container_name: transit-sync
    profiles:
      - oneshot
    restart: \"no\"
    command: [\"python\", \"sync_gtfs.py\"]
    env_file:
      - ${BASE_DIR}/transit/transit.env
    volumes:
      - ${BASE_DIR}/transit/data:/data
    environment:
      - TZ=${TIMEZONE}
"
fi
if [[ "$ENABLE_TRIP_PLANNING" == "true" ]]; then
  # Official OpenTripPlanner image. VERIFY the exact CLI flags/serve command against
  # whichever OTP version you deploy — this repo does not build or manage the graph
  # itself, see transit/README.md's 'Route planning scope'.
  OTP_BLOCK="
  otp:
    image: opentripplanner/opentripplanner:latest
    container_name: otp
    restart: unless-stopped
    ports:
      - \"${OTP_PORT}:8080\"
    volumes:
      - ${OTP_GRAPHS_DIR}:/var/opentripplanner
    command: [\"--load\", \"--serve\"]
    environment:
      - TZ=${TIMEZONE}
"
fi

# chores (Phase 20) — oneshot, driven by smart-home-chores.timer below, same
# profiles:[oneshot] shape as trash-calendar/transit-sync above. Reads from
# identity/Frigate/ntfy over the network, not container DNS — no shared volume
# needed with any of them.
CHORES_BLOCK=""
if [[ "$ENABLE_CHORES" == "true" ]]; then
  CHORES_BLOCK="
  chores:
    build: ${CHORES_SRC}
    image: smart-home/chores:local
    container_name: chores
    profiles:
      - oneshot
    restart: \"no\"
    env_file:
      - ${BASE_DIR}/chores/chores.env
    volumes:
      - ${BASE_DIR}/chores/data:/data
    environment:
      - TZ=${TIMEZONE}
"
fi

# Music Assistant — own container, host networking (see ENABLE_MUSIC_ASSISTANT's
# module-level comment above for why). No published-port line needed since host
# networking exposes its own default port (8095) directly.
MUSIC_ASSISTANT_BLOCK=""
if [[ "$ENABLE_MUSIC_ASSISTANT" == "true" ]]; then
  MUSIC_ASSISTANT_BLOCK="
  music-assistant:
    image: ghcr.io/music-assistant/server:stable
    container_name: music-assistant
    restart: unless-stopped
    network_mode: host
    volumes:
      - ${BASE_DIR}/music-assistant/data:/data
    environment:
      - TZ=${TIMEZONE}
"
fi

if [[ "$ENABLE_DIGEST_ENGINE" == "true" && "$ENABLE_WHATSAPP_INGEST" == "true" ]]; then
  # Long-lived, unlike digest-engine: holds the logged-in WhatsApp Web session
  # open and appends to /data/messages.jsonl, which digest-engine drains each
  # run. No ports — never reachable from the LAN, only via the shared volume.
  # shm_size is required: Chromium's renderer crashes on Docker's default 64MB.
  WHATSAPP_BRIDGE_BLOCK="
  whatsapp-bridge:
    build: ${DIGEST_ENGINE_SRC}/whatsapp-bridge
    image: smart-home/whatsapp-bridge:local
    container_name: whatsapp-bridge
    restart: unless-stopped
    shm_size: \"512mb\"
    volumes:
      - ${BASE_DIR}/digest/data/whatsapp-bridge:/data
      - /etc/localtime:/etc/localtime:ro
    environment:
      - TZ=${TIMEZONE}
"
fi

cat > "$BASE_DIR/docker-compose.yml" <<EOF
name: smart-home

services:
  homeassistant:
    image: ghcr.io/home-assistant/home-assistant:stable
    container_name: homeassistant
    restart: unless-stopped
    privileged: true
    network_mode: host
    volumes:
      - ${BASE_DIR}/homeassistant:/config
      - /etc/localtime:/etc/localtime:ro
    environment:
      - TZ=${TIMEZONE}

  mosquitto:
    image: eclipse-mosquitto:2
    container_name: mosquitto
    restart: unless-stopped
    ports:
      - "1883:1883"
    volumes:
      - ${BASE_DIR}/mosquitto/config:/mosquitto/config
      - ${BASE_DIR}/mosquitto/data:/mosquitto/data
      - ${BASE_DIR}/mosquitto/log:/mosquitto/log

  zigbee2mqtt:
    image: koenkk/zigbee2mqtt:latest
    container_name: zigbee2mqtt
    restart: unless-stopped
    depends_on:
      - mosquitto
    ports:
      - "8080:8080"
    volumes:
      - ${BASE_DIR}/zigbee2mqtt/data:/app/data
      - /etc/localtime:/etc/localtime:ro
    devices:
      - ${ZIGBEE_USB_DEVICE}:/dev/ttyACM0
    environment:
      - TZ=${TIMEZONE}

  frigate:
    image: ghcr.io/blakeblackshear/frigate:stable
    container_name: frigate
    restart: unless-stopped
    privileged: true
    shm_size: "256mb"
    depends_on:
      - mosquitto
    ports:
      - "5000:5000"
      - "8554:8554"
      - "8555:8555/tcp"
      - "8555:8555/udp"
    volumes:
      - ${BASE_DIR}/frigate/config:/config
      - ${BASE_DIR}/frigate/media:/media/frigate
      - /etc/localtime:/etc/localtime:ro
${FRIGATE_DEVICES}
    environment:
      - TZ=${TIMEZONE}

  grocy:
    image: lscr.io/linuxserver/grocy:latest
    container_name: grocy
    restart: unless-stopped
    ports:
      - "9283:80"
    volumes:
      - ${BASE_DIR}/grocy/config:/config
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=${TIMEZONE}
${MEALIE_BLOCK}${NODERED_BLOCK}${NETDATA_BLOCK}${HOMEPAGE_BLOCK}${NTFY_BLOCK}${PORTAINER_BLOCK}${GALLERY_SMB_BLOCK}${DIGEST_ENGINE_BLOCK}${DIGEST_WEB_BLOCK}${WHATSAPP_BRIDGE_BLOCK}${ADMIN_CANVAS_BLOCK}${ADMIN_WEB_BLOCK}${PANTRY_VISION_BLOCK}${PANTRY_WEB_BLOCK}${IDENTITY_BLOCK}${IDENTITY_WEB_BLOCK}${PROXY_BLOCK}${TRASH_CALENDAR_BLOCK}${TRANSIT_BLOCK}${TRANSIT_SYNC_BLOCK}${OTP_BLOCK}${CHORES_BLOCK}${MUSIC_ASSISTANT_BLOCK}${PHOTOS_WEB_BLOCK}${WORKSHOP_BLOCK}${WORKSHOP_WEB_BLOCK}
EOF

# ---------------------------------------------------------------------------
# 9. Scheduled backups (restic) — optional
# ---------------------------------------------------------------------------
if [[ "$ENABLE_BACKUPS" == "true" ]]; then
  echo "--- Setting up restic backups ---"

  RESTIC_PASSWORD_FILE="$BASE_DIR/.restic-password"
  if [[ ! -f "$RESTIC_PASSWORD_FILE" ]]; then
    openssl rand -base64 32 > "$RESTIC_PASSWORD_FILE"
    chmod 600 "$RESTIC_PASSWORD_FILE"
    echo "  Generated a new restic repository password at $RESTIC_PASSWORD_FILE."
    echo "  BACK THIS FILE UP SOMEWHERE ELSE — losing it makes the backup repo unreadable."
  fi

  cat > "$BASE_DIR/backup.env" <<EOF
RESTIC_REPOSITORY=${RESTIC_REPOSITORY}
RESTIC_PASSWORD_FILE=${RESTIC_PASSWORD_FILE}
EOF
  chmod 600 "$BASE_DIR/backup.env"

  cat > "$BASE_DIR/backup.sh" <<'BACKUP_EOF'
#!/usr/bin/env bash
# Backs up all stateful smart-home volumes with restic.
# Stops the stack briefly for a consistent snapshot of SQLite-backed configs
# (HA, Zigbee2MQTT, Grocy, Node-RED), then restarts it.
# Frigate's recorded video is excluded — it's large and non-critical to keep;
# face-recognition embeddings live under frigate/config, which IS backed up.
set -euo pipefail
BASE_DIR="/opt/smart-home"
source "$BASE_DIR/backup.env"

cd "$BASE_DIR"

if ! restic snapshots --no-lock >/dev/null 2>&1; then
  echo "Initializing new restic repository at $RESTIC_REPOSITORY"
  restic init
fi

echo "Stopping stack for a consistent backup..."
docker compose stop

restic backup "$BASE_DIR" \
  --exclude "$BASE_DIR/frigate/media" \
  --exclude "$BASE_DIR/.restic-password" \
  --exclude "$BASE_DIR/backup.env"

echo "Restarting stack..."
docker compose start

restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
BACKUP_EOF
  chmod +x "$BASE_DIR/backup.sh"

  cat > /etc/systemd/system/smart-home-backup.service <<EOF
[Unit]
Description=Smart home restic backup

[Service]
Type=oneshot
ExecStart=${BASE_DIR}/backup.sh
EOF

  cat > /etc/systemd/system/smart-home-backup.timer <<EOF
[Unit]
Description=Daily smart home restic backup

[Timer]
OnCalendar=*-*-* ${BACKUP_SCHEDULE}:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

  systemctl daemon-reload
  systemctl enable --now smart-home-backup.timer
  echo "  Backup timer installed: daily at ${BACKUP_SCHEDULE}, repository: ${RESTIC_REPOSITORY}"
  echo "  Run it manually any time with: sudo $BASE_DIR/backup.sh"
fi

# ---------------------------------------------------------------------------
# 9b. Scheduled quarter-daily digest (Phase 12) — optional, mirrors the restic
#     timer above
# ---------------------------------------------------------------------------
if [[ "$ENABLE_DIGEST_ENGINE" == "true" ]]; then
  echo "--- Setting up the quarter-daily digest timer ---"

  cat > /etc/systemd/system/smart-home-digest.service <<EOF
[Unit]
Description=Smart home quarter-daily LLM digest
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
WorkingDirectory=${BASE_DIR}
ExecStart=/usr/bin/docker compose run --rm digest-engine
EOF

  cat > /etc/systemd/system/smart-home-digest.timer <<EOF
[Unit]
Description=Quarter-daily smart home LLM digest

[Timer]
OnCalendar=*-*-* ${DIGEST_SCHEDULE}:00:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

  systemctl daemon-reload
  systemctl enable --now smart-home-digest.timer
  echo "  Digest timer installed: 4x/day at ${DIGEST_SCHEDULE}:00."
  echo "  Run it manually any time with: cd $BASE_DIR && docker compose run --rm digest-engine"
fi

# ---------------------------------------------------------------------------
# 9c. Scheduled trash-calendar sync (Phase 19) — optional, mirrors the digest timer
# ---------------------------------------------------------------------------
if [[ "$ENABLE_TRASH_CALENDAR" == "true" ]]; then
  echo "--- Setting up the daily trash-calendar sync timer ---"

  cat > /etc/systemd/system/smart-home-trash-calendar.service <<EOF
[Unit]
Description=Smart home trash collection calendar sync
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
WorkingDirectory=${BASE_DIR}
ExecStart=/usr/bin/docker compose run --rm trash-calendar
EOF

  cat > /etc/systemd/system/smart-home-trash-calendar.timer <<EOF
[Unit]
Description=Daily smart home trash collection calendar sync

[Timer]
OnCalendar=*-*-* 06:15:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

  systemctl daemon-reload
  systemctl enable --now smart-home-trash-calendar.timer
  echo "  Trash-calendar timer installed: daily at 06:15."
  echo "  Run it manually any time with: cd $BASE_DIR && docker compose run --rm trash-calendar"
fi

# ---------------------------------------------------------------------------
# 9d. Scheduled GTFS refresh for transit (Phase 19) — optional, weekly (schedules
#     are published a season/year at a time, not daily)
# ---------------------------------------------------------------------------
if [[ "$ENABLE_TRANSIT" == "true" ]]; then
  echo "--- Setting up the weekly transit GTFS refresh timer ---"

  cat > /etc/systemd/system/smart-home-transit-sync.service <<EOF
[Unit]
Description=Smart home transit GTFS refresh
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
WorkingDirectory=${BASE_DIR}
ExecStart=/usr/bin/docker compose run --rm transit-sync
EOF

  cat > /etc/systemd/system/smart-home-transit-sync.timer <<EOF
[Unit]
Description=Weekly smart home transit GTFS refresh

[Timer]
OnCalendar=Mon *-*-* 04:00:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

  systemctl daemon-reload
  systemctl enable --now smart-home-transit-sync.timer
  echo "  Transit GTFS sync timer installed: weekly, Monday 04:00."
  echo "  Run it manually any time with: cd $BASE_DIR && docker compose run --rm transit-sync"
fi

# ---------------------------------------------------------------------------
# 9e. Scheduled chore checks (Phase 20) — optional, every 2 hours. RandomizedDelaySec
#     is systemd's own built-in jitter and is exactly the "+/-30 min in case
#     something else is running" this was specified with — not custom code.
# ---------------------------------------------------------------------------
if [[ "$ENABLE_CHORES" == "true" ]]; then
  echo "--- Setting up the every-2-hours chore check timer ---"

  cat > /etc/systemd/system/smart-home-chores.service <<EOF
[Unit]
Description=Smart home chore distribution + reminder + camera-verification check
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
WorkingDirectory=${BASE_DIR}
ExecStart=/usr/bin/docker compose run --rm chores
EOF

  cat > /etc/systemd/system/smart-home-chores.timer <<EOF
[Unit]
Description=Every-2-hours smart home chore check

[Timer]
OnCalendar=*-*-* 00/2:00:00
RandomizedDelaySec=1800
Persistent=true

[Install]
WantedBy=timers.target
EOF

  systemctl daemon-reload
  systemctl enable --now smart-home-chores.timer
  echo "  Chores timer installed: every 2 hours, +/- up to 30 min jitter."
  echo "  Run it manually any time with: cd $BASE_DIR && docker compose run --rm chores"
fi

# ---------------------------------------------------------------------------
# 10. Bring the stack up
# ---------------------------------------------------------------------------
echo "--- Starting containers ---"
cd "$BASE_DIR"
docker compose up -d

echo
echo "=== Done ==="
echo "Services should be reachable at:"
echo "  Home Assistant : http://${HOST_IP}:8123"
echo "  Zigbee2MQTT UI  : http://${HOST_IP}:8080"
echo "  Frigate         : http://${HOST_IP}:5000"
echo "  Grocy           : http://${HOST_IP}:9283"
if [[ "$ENABLE_MEALIE" == "true" ]]; then
  echo "  Mealie          : http://${HOST_IP}:9925"
fi
if [[ "$ENABLE_NODERED" == "true" ]]; then
  echo "  Node-RED        : http://${HOST_IP}:1880"
fi
if [[ "$ENABLE_NETDATA" == "true" ]]; then
  echo "  Netdata         : http://${HOST_IP}:19999"
fi
if [[ "$ENABLE_HOMEPAGE" == "true" ]]; then
  echo "  Homepage        : http://${HOST_IP}:3000"
fi
if [[ "$ENABLE_NTFY" == "true" ]]; then
  echo "  ntfy            : http://${HOST_IP}:8090"
fi
if [[ "$ENABLE_PORTAINER" == "true" ]]; then
  echo "  Portainer       : http://${HOST_IP}:9000"
fi
if [[ "$ENABLE_DIGEST_ENGINE" == "true" ]]; then
  echo "  Digest (full)   : http://${HOST_IP}:${DIGEST_WEB_PORT}/full.html"
  echo "  Digest (compact): http://${HOST_IP}:${DIGEST_WEB_PORT}/compact.html"
fi
if [[ "$ENABLE_ADMIN_CANVAS" == "true" ]]; then
  echo "  Admin canvas    : http://${HOST_IP}:${ADMIN_WEB_PORT}/canvas.html"
fi
if [[ "$ENABLE_PANTRY_VISION" == "true" ]]; then
  echo "  Pantry API      : http://${HOST_IP}:${PANTRY_VISION_PORT} (bearer-token gated)"
  echo "  Pantry display  : http://${HOST_IP}:${PANTRY_WEB_PORT}/index.html?api=http://${HOST_IP}:${PANTRY_VISION_PORT}&token=<PANTRY_VISION_TOKEN>"
fi
if [[ "$ENABLE_IDENTITY" == "true" ]]; then
  echo "  Identity API    : http://${HOST_IP}:${IDENTITY_PORT} (bearer-token gated)"
  echo "  Register page   : http://${HOST_IP}:${IDENTITY_WEB_PORT}/register.html?api=http://${HOST_IP}:${IDENTITY_PORT}&token=<IDENTITY_TOKEN>&device=<name>"
  echo "  Dashboard page  : http://${HOST_IP}:${IDENTITY_WEB_PORT}/dashboard.html?identity_api=http://${HOST_IP}:${IDENTITY_PORT}&identity_token=<IDENTITY_TOKEN>"
  if [[ "$ENABLE_PROXY" == "true" ]]; then
    echo "  Admin panel     : https://${PROXY_HOSTNAME}/admin.html?api=https://${PROXY_HOSTNAME}/api/identity&token=<IDENTITY_TOKEN>"
    echo "                    (HTTP redirects to HTTPS. With tls=internal, install the root"
    echo "                     CA first or the browser will warn: tools/export-proxy-ca.sh)"
  else
    echo "  Admin panel     : http://${HOST_IP}:${IDENTITY_WEB_PORT}/admin.html?api=http://${HOST_IP}:${IDENTITY_PORT}&token=<IDENTITY_TOKEN>"
    echo "                    NOTE: plain HTTP puts the admin token on the wire in clear."
    echo "                     Set ENABLE_PROXY=true for HTTPS — see proxy/README.md"
  fi
  echo "                    (people/guests, pruning, visit history, device rights —"
  echo "                     NOT a kiosk page; keep this URL off the wall panels)"
fi
if [[ "$ENABLE_GALLERY_SMB" == "true" ]]; then
  echo "  Gallery SMB     : \\\\${HOST_IP}\\gallery (user: ${GALLERY_SMB_USERNAME})"
fi
echo
echo "Next steps:"
echo "  1. Edit $BASE_DIR/frigate/config/config.yml with your real peephole camera RTSP URL, then:"
echo "       docker compose -f $BASE_DIR/docker-compose.yml restart frigate"
echo "  2. Check 'docker logs zigbee2mqtt' — if it can't open the serial port, confirm"
echo "     ZIGBEE_USB_DEVICE at the top of this script matches 'ls -l /dev/serial/by-id/',"
echo "     and that the dongle has Z-Stack coordinator firmware (not stock gateway firmware)."
echo "  3. Complete Home Assistant's onboarding wizard at :8123."
echo "  4. In HA, add the MQTT integration (it should auto-discover Zigbee2MQTT devices)."
echo "  5. Portainer: open :9000 within a few minutes of first start to set the admin password"
echo "     before anyone else on the LAN can claim that instance."
echo "  6. Node-RED runs in its own bridge network, but Home Assistant uses network_mode: host,"
echo "     so Node-RED can't reach it by container name — use http://${HOST_IP}:8123 plus a"
echo "     long-lived access token (HA profile page) when wiring up the HA nodes."
echo "  7. Add the CalDAV integration pointing at your Nextcloud instance."
echo "  8. Point HA's Ollama conversation integration at your separate GPU/LLM host."
if [[ "$ENABLE_BACKUPS" != "true" ]]; then
  echo "  9. Backups are off. Set ENABLE_BACKUPS=true and RESTIC_REPOSITORY at the top of this"
  echo "     script and re-run once you have backup storage (external drive, NAS, or remote) ready."
fi
if [[ "$ENABLE_DIGEST_ENGINE" == "true" ]]; then
  echo " 10. Fill in $BASE_DIR/digest/digest-engine.env before the first digest run."
  echo " 11. Telegram needs a one-time interactive login:"
  echo "       cd $BASE_DIR && docker compose run --rm --entrypoint python digest-engine ingest/telegram_login.py"
  if [[ "$ENABLE_WHATSAPP_INGEST" == "true" ]]; then
    echo " 12. WhatsApp needs a one-time QR scan: docker compose logs -f whatsapp-bridge"
  fi
  echo "     Add the compact view to HA as a Lovelace iframe card pointing at"
  echo "     http://${HOST_IP}:${DIGEST_WEB_PORT}/compact.html"
fi
if [[ "$ENABLE_GALLERY_SMB" == "true" ]]; then
  echo " 13. Copy photos into $BASE_DIR/gallery — an empty share is harmless, thin clients"
  echo "     just skip the idle slideshow until there's something in it."
  echo "     On each thin client, put the SAME ${GALLERY_SMB_USERNAME}/${GALLERY_SMB_PASSWORD:+<password>}"
  echo "     you set at the top of this script into /etc/thinclient-agent/gallery-credentials"
  echo "     — see hosts/thin-client/README.md."
fi
if [[ "$ENABLE_ADMIN_CANVAS" == "true" ]]; then
  echo " 14. Fill in $BASE_DIR/admin-canvas/admin-canvas.env before the sys-admin-llm can push"
  echo "     anything (ADMIN_CANVAS_TOKEN is required — the service rejects every request"
  echo "     while it is empty). Paste the same token into HA's rest_command: config — see"
  echo "     admin-canvas/README.md for the worked example and the 'Show admin canvas'"
  echo "     entity documented in hosts/thin-client/README.md."
fi
if [[ "$ENABLE_PANTRY_VISION" == "true" ]]; then
  echo " 15. Fill in $BASE_DIR/pantry-vision/pantry-vision.env before the kitchen display can"
  echo "     identify anything: PANTRY_VISION_TOKEN (also goes into"
  echo "     tools/build-kitchen-display-iso.sh — both sides need the"
  echo "     SAME value) and GROCY_API_KEY (Grocy's own UI: Settings -> Manage API keys, at"
  echo "     http://${HOST_IP}:9283). Also pull a vision-capable Ollama model on the LLM host"
  echo "     (e.g. 'ollama pull llava') — OLLAMA_VISION_MODEL defaults to one that is NOT"
  echo "     confirmed pulled or even correct for your setup. See pantry-vision/README.md."
fi
if [[ "$ENABLE_IDENTITY" == "true" ]]; then
  echo " 16. Fill in $BASE_DIR/identity/identity.env before registration/presence can work:"
  echo "     IDENTITY_TOKEN (also goes into hosts/kitchen-display's and"
  echo "     hosts/door-panel's build scripts — every side needs the SAME value), HA_URL"
  echo "     (this host's real LAN IP, NOT 'homeassistant' — see the .env template's own"
  echo "     comment for why), HA_TOKEN (HA's own UI: profile -> Security -> Long-Lived"
  echo "     Access Tokens), and TRUSTED_ENTITY_PREFIXES (Developer Tools -> States, after"
  echo "     Bermuda's Private BLE Device integration and/or fixed BLE tags are set up —"
  echo "     see docs/project-plan.md §1.5). See identity/README.md for the worked HA"
  echo "     custom-sentence/intent-script example that makes 'register me as <name>' work."
fi
echo
echo "Updating later: cd $BASE_DIR && docker compose pull && docker compose up -d"
echo "Backing up manually: sudo $BASE_DIR/backup.sh (requires ENABLE_BACKUPS=true was run once)"
