locals {
  # A Secure cookie is only sent over HTTPS, so enable it only where Caddy terminates
  # TLS (domain set). A plain-HTTP dev box (domain "") would otherwise drop its login
  # cookie and no one could stay signed in.
  cookie_secure = var.domain != "" ? "1" : "0"

  # Public base URL: the domain over HTTPS, else the raw host:port for a Caddy-less box.
  base_url = var.domain != "" ? "https://${var.domain}" : "http://${var.host}:${var.app_port}"

  # Layer the self-hosted Open-Meteo overlay on hosts that self-host the archive.
  effective_compose_files = var.openmeteo ? concat(var.compose_files, ["docker-compose.openmeteo.yml"]) : var.compose_files

  # `-f a -f b` for the compose invocations (dev layers the dev overlay).
  compose_flags = join(" ", [for f in local.effective_compose_files : "-f ${f}"])

  # Hash the local compose files so a compose edit re-triggers the remote deploy.
  compose_files_sha = join(",", [for f in local.effective_compose_files : filesha256("${var.repo_root}/${f}")])

  # Rendered /etc/thermograph-topology.env — non-secret sizing/routing config only.
  # Every app secret is rendered separately, at deploy time, from the SOPS+age
  # vault (see remote-exec step 3 below) — Terraform never sees or carries them.
  topology_env_content = templatefile("${path.module}/templates/thermograph-topology.env.tftpl", {
    app_port        = var.app_port
    workers         = var.workers
    app_cpus        = var.app_cpus
    db_cpus         = var.db_cpus
    db_memory       = var.db_memory
    timescaledb_tag = var.timescaledb_tag
    base            = "/"
    base_url        = local.base_url
    cookie_secure   = local.cookie_secure
    openmeteo       = var.openmeteo
    om_data_dir     = var.om_data_dir
  })

  # Caddyfile is only meaningful on a host with a public domain.
  caddy_content = var.domain != "" ? templatefile("${path.module}/templates/Caddyfile.tftpl", {
    domain        = var.domain
    port          = var.app_port
    frontend_port = var.frontend_port
  }) : "# No public domain on this host; Caddy is not managed here.\n"

  # systemd unit that keeps the object-storage bucket rclone-mounted at om_data_dir,
  # so the Open-Meteo containers read the ERA5 .om archive from it. Only installed on
  # openmeteo hosts; --allow-other lets the container (root) read the FUSE mount.
  rclone_unit = <<-UNIT
    [Unit]
    Description=rclone mount ERA5 archive (Thermograph)
    After=network-online.target
    Wants=network-online.target

    [Service]
    Type=notify
    ExecStartPre=/bin/mkdir -p ${var.om_data_dir}
    ExecStart=/usr/bin/rclone mount ${var.om_bucket_remote} ${var.om_data_dir} --config /etc/rclone/rclone.conf --vfs-cache-mode full --vfs-cache-max-size ${var.om_vfs_cache_max} --dir-cache-time 12h --allow-other --umask 000
    ExecStop=/bin/fusermount -u ${var.om_data_dir}
    Restart=on-failure
    RestartSec=5

    [Install]
    WantedBy=multi-user.target
  UNIT

  rclone_unit_content = var.openmeteo ? local.rclone_unit : "# openmeteo disabled on this host\n"
}

resource "null_resource" "host" {
  # Re-provision when the rendered topology env, the compose files, the branch,
  # sizing, the app image tag, or the Caddyfile change.
  triggers = {
    topology_env_sha   = sha256(local.topology_env_content)
    compose_sha        = local.compose_files_sha
    compose_flags      = local.compose_flags
    caddy_sha          = sha256(local.caddy_content)
    branch             = var.git_branch
    backend_image_tag  = var.backend_image_tag
    frontend_image_tag = var.frontend_image_tag
    app_dir            = var.app_dir
    sizing             = "${var.workers}/${var.app_cpus}/${var.db_cpus}/${var.db_memory}/${var.timescaledb_tag}"
    # Re-provision when the archive self-hosting config changes. The rclone.conf is
    # hashed (nonsensitive on a one-way digest) so a credential rotation redeploys.
    openmeteo = "${var.openmeteo}/${var.om_data_dir}/${var.om_bucket_remote}/${var.om_vfs_cache_max}"
    om_conf   = var.openmeteo ? nonsensitive(sha256(var.om_rclone_conf)) : "off"
  }

  connection {
    type        = "ssh"
    host        = var.host
    user        = var.ssh_user
    private_key = file(pathexpand(var.ssh_private_key_path))
    timeout     = "5m"
  }

  # Push the rendered topology env via `content` (consistent with the other
  # `file` provisioners here, though nothing in it is secret).
  provisioner "file" {
    content     = local.topology_env_content
    destination = "/tmp/thermograph-topology.env"
  }

  provisioner "file" {
    content     = local.caddy_content
    destination = "/tmp/thermograph.Caddyfile"
  }

  # rclone config (bucket credentials) + the mount unit. Pushed via `content` so the
  # secret never touches local disk; harmless placeholders on non-openmeteo hosts.
  provisioner "file" {
    content     = var.openmeteo ? var.om_rclone_conf : "# openmeteo disabled on this host\n"
    destination = "/tmp/thermograph.rclone.conf"
  }

  provisioner "file" {
    content     = local.rclone_unit_content
    destination = "/tmp/rclone-om.service"
  }

  # 1. Host setup: Docker + compose plugin, ufw firewall, and (domain hosts) Caddy.
  provisioner "remote-exec" {
    inline = [
      <<-EOT
        set -eu
        echo "[${var.name}] setup: docker, compose plugin, firewall"
        if ! command -v docker >/dev/null 2>&1; then
          curl -fsSL https://get.docker.com | sudo sh
        fi
        sudo usermod -aG docker "$(id -un)" || true
        if ! sudo docker compose version >/dev/null 2>&1; then
          sudo apt-get update -y
          sudo apt-get install -y docker-compose-plugin
        fi
        if ! command -v ufw >/dev/null 2>&1; then
          sudo apt-get update -y
          sudo apt-get install -y ufw
        fi
        sudo ufw allow 22/tcp
        sudo ufw allow 80/tcp
        sudo ufw allow 443/tcp
        DOMAIN='${var.domain}'
        if [ -z "$DOMAIN" ]; then
          sudo ufw allow ${var.app_port}/tcp
        fi
        sudo ufw --force enable
        if [ -n "$DOMAIN" ]; then
          if ! command -v caddy >/dev/null 2>&1; then
            sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
            curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
            curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null
            sudo apt-get update -y
            sudo apt-get install -y caddy
          fi
          sudo install -m 0644 /tmp/thermograph.Caddyfile /etc/caddy/Caddyfile
          sudo systemctl reload caddy || sudo systemctl restart caddy
        fi
        rm -f /tmp/thermograph.Caddyfile
      EOT
    ]
  }

  # 1b. Self-hosted archive: rclone-mount the ERA5 bucket before compose up so
  #     open-meteo-api can read .om from object storage. Skipped on non-openmeteo hosts.
  provisioner "remote-exec" {
    inline = [
      <<-EOT
        set -eu
        if [ "${var.openmeteo}" != "true" ]; then
          rm -f /tmp/thermograph.rclone.conf /tmp/rclone-om.service
          # Undo any prior openmeteo setup so a toggled-off host doesn't wait on a mount.
          if [ -e /etc/systemd/system/docker.service.d/10-wait-rclone.conf ]; then
            sudo rm -f /etc/systemd/system/docker.service.d/10-wait-rclone.conf
            sudo systemctl disable --now rclone-om >/dev/null 2>&1 || true
            sudo systemctl daemon-reload
          fi
          echo "[${var.name}] openmeteo: disabled"
          exit 0
        fi
        echo "[${var.name}] openmeteo: rclone mount ${var.om_bucket_remote} -> ${var.om_data_dir}"
        if ! command -v rclone >/dev/null 2>&1; then
          curl -fsSL https://rclone.org/install.sh | sudo bash
        fi
        # FUSE allow_other so the container (root) can read a mount owned by this user.
        if ! grep -q '^user_allow_other' /etc/fuse.conf 2>/dev/null; then
          echo user_allow_other | sudo tee -a /etc/fuse.conf >/dev/null
        fi
        sudo install -d -m 0755 /etc/rclone
        sudo install -m 0600 /tmp/thermograph.rclone.conf /etc/rclone/rclone.conf
        sudo install -m 0644 /tmp/rclone-om.service /etc/systemd/system/rclone-om.service
        rm -f /tmp/thermograph.rclone.conf /tmp/rclone-om.service
        sudo install -d -m 0755 ${var.om_data_dir}
        sudo systemctl daemon-reload
        sudo systemctl enable rclone-om
        sudo systemctl restart rclone-om
        # Wait for the mount before compose bind-mounts it.
        i=0
        while [ "$i" -lt 30 ]; do
          if mountpoint -q ${var.om_data_dir}; then break; fi
          i=$((i + 1)); sleep 2
        done
        if ! mountpoint -q ${var.om_data_dir}; then
          echo "[${var.name}] RCLONE MOUNT FAILED" >&2
          sudo systemctl status rclone-om --no-pager || true
          exit 1
        fi
        # Order Docker after the mount on every boot, so the restart-policy containers
        # never bind an empty mount point. rclone-om is Type=notify, so `After` waits
        # until the mount is actually ready — not merely that the unit was launched.
        sudo install -d -m 0755 /etc/systemd/system/docker.service.d
        printf '[Unit]\nWants=rclone-om.service\nAfter=rclone-om.service\n' \
          | sudo tee /etc/systemd/system/docker.service.d/10-wait-rclone.conf >/dev/null
        sudo systemctl daemon-reload
        echo "[${var.name}] openmeteo: mounted at ${var.om_data_dir}"
      EOT
    ]
  }

  # 2. Sync the checkout to the host's branch (cloning first on a fresh box). This
  #    is now the INFRA repo's checkout (compose files, db init, Caddy templates,
  #    the secrets vault, deploy.sh itself) — the app's own source is never checked
  #    out on the host; only its published registry image is pulled (step 3).
  provisioner "remote-exec" {
    inline = [
      <<-EOT
        set -eu
        echo "[${var.name}] code: ${var.app_dir} -> origin/${var.git_branch}"
        sudo mkdir -p ${var.app_dir}
        sudo chown -R "$(id -un)":"$(id -gn)" ${var.app_dir}
        if [ ! -d ${var.app_dir}/.git ]; then
          git clone ${var.repo_url} ${var.app_dir}
        fi
        cd ${var.app_dir}
        git fetch --prune origin ${var.git_branch}
        git reset --hard origin/${var.git_branch}
      EOT
    ]
  }

  # 3. Install /etc/thermograph-topology.env, render /etc/thermograph.env from the
  #    SOPS+age vault (deploy/render-secrets.sh, now part of this same checkout),
  #    then bring the stack up on the EXPLICIT per-service app image tags — there's no
  #    app checkout on this host to derive a tag from (see var.backend_image_tag /
  #    frontend_image_tag). The compose file reads BACKEND_IMAGE_TAG / FRONTEND_IMAGE_TAG
  #    (for emi/thermograph-backend/app and emi/thermograph-frontend/app), so we export
  #    those, not the old single IMAGE_PATH/IMAGE_TAG. docker runs as root (sources both
  #    env files in the same shell) so it never depends on the docker group membership
  #    taking effect in this session.
  provisioner "remote-exec" {
    inline = [
      <<-EOT
        set -eu
        echo "[${var.name}] deploy: install topology env + render secrets + docker compose pull + up"
        sudo install -m 0640 -o root -g root /tmp/thermograph-topology.env /etc/thermograph-topology.env
        rm -f /tmp/thermograph-topology.env
        sudo bash -c '
          set -eu
          cd ${var.app_dir}
          . deploy/render-secrets.sh
          render_thermograph_secrets ${var.app_dir}
          set -a; . /etc/thermograph-topology.env; . /etc/thermograph.env; set +a
          export REGISTRY_HOST="git.thermograph.org" BACKEND_IMAGE_TAG="${var.backend_image_tag}" FRONTEND_IMAGE_TAG="${var.frontend_image_tag}"
          echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username emi --password-stdin
          docker compose ${local.compose_flags} pull backend frontend
          docker compose ${local.compose_flags} up -d --remove-orphans
        '
      EOT
    ]
  }

  # 4. Health check backend on loopback -- a plain "/" here already exercises
  #    the whole chain end to end even without Caddy in front (backend's own
  #    reverse-proxy fallback forwards to frontend), so one curl covers both
  #    services in every topology this module supports (repo-split Stage 4).
  provisioner "remote-exec" {
    inline = [
      <<-EOT
        set -eu
        echo "[${var.name}] health: http://127.0.0.1:${var.app_port}/"
        ok=0
        i=0
        while [ "$i" -lt 30 ]; do
          if curl -fsS -o /dev/null "http://127.0.0.1:${var.app_port}/"; then ok=1; break; fi
          i=$((i + 1))
          sleep 2
        done
        if [ "$ok" != 1 ]; then
          echo "[${var.name}] HEALTH CHECK FAILED" >&2
          sudo bash -c 'cd ${var.app_dir} && docker compose ${local.compose_flags} ps; docker compose ${local.compose_flags} logs --tail=50 backend; docker compose ${local.compose_flags} logs --tail=50 frontend' || true
          exit 1
        fi
        echo "[${var.name}] OK: serving on 127.0.0.1:${var.app_port}"
        if ! curl -fsS -o /dev/null "http://127.0.0.1:${var.frontend_port}/healthz"; then
          echo "[${var.name}] frontend's own /healthz failed directly (backend's proxy to it may still work) -- check separately" >&2
          sudo bash -c 'cd ${var.app_dir} && docker compose ${local.compose_flags} logs --tail=50 frontend' || true
        fi
      EOT
    ]
  }
}

output "host" {
  description = "Address this module manages."
  value       = var.host
}

output "role" {
  description = "Role of this host."
  value       = var.role
}
