;;; Monadic chain example — dynamic graph via (resolve ...)
;;;
;;; Same three-service stack as compose.reml (db → migrate → app),
;;; using the dynamic executor instead of run.
;;;
;;; Key difference from compose.reml:
;;;
;;;   compose.reml       — static: full graph declared before execution;
;;;                        run topo-sorts and cycle-detects upfront;
;;;                        ContainerHandle.deps populated → cascade teardown.
;;;
;;;   compose-chain.reml — dynamic: each (then ...) lambda returns the
;;;                        *next* node; the graph unfolds as execution
;;;                        proceeds.  No upfront graph declaration needed.
;;;                        resolve does not populate deps → no cascade teardown.
;;;
;;; When to use each:
;;;
;;;   run     — complex multi-service graphs, independent services that
;;;             can run in parallel (db ∥ cache), or when you want upfront
;;;             cycle detection and automatic cascade teardown.
;;;
;;;   resolve — linear pipelines where each step depends on the *value*
;;;             produced by the previous step, and the next node cannot
;;;             be described until that value is known.
;;;
;;; Cleanup in the chain model:
;;;
;;;   The chain model deliberately hides intermediate ContainerHandles —
;;;   the lambda receives a handle only for its immediate upstream, and
;;;   only while it runs.  After resolve returns, those handles are gone.
;;;
;;;   Two mechanisms cover cleanup:
;;;
;;;     1. Interpreter Drop (implicit): when the Interpreter goes out of
;;;        scope — whether the script completes normally or exits via error —
;;;        it sends SIGTERM to every container still in the registry, waits
;;;        up to 5 s for graceful exit, then SIGKILLs any stragglers, and
;;;        finally joins all background threads.  This is the safety net
;;;        that makes the chain model safe to use.
;;;
;;;     2. with-cleanup (explicit, this file): fires on both normal exit
;;;        and error; handles logging and any teardown that *does* need
;;;        an explicit handle (here: none, because all containers are
;;;        covered by the implicit registry cleanup).
;;;
;;;   Contrast with compose.reml, where run populates ContainerHandle.deps
;;;   and container-wait cascades teardown explicitly in reverse topo order.
;;;
;;; Dependency graph:
;;;
;;;   db ──→ (then: compute db-url) ──→ migrate ──→ (then: start app) ──→ app
;;;
;;; Usage:
;;;   sudo -E pelagos compose up -f compose-chain.reml -p chain-demo

;; ── Service declarations ──────────────────────────────────────────────────

(define-service svc-db "db"
  :image   "postgres:16"
  :network "app-net"
  :env     ("POSTGRES_PASSWORD" . "secret")
           ("POSTGRES_DB"       . "appdb")
           ("POSTGRES_USER"     . "app"))

(define-service svc-migrate "migrate"
  :image   "alpine:latest"
  :network "app-net"
  :command "/bin/sh" "-c"
           "echo \"migrating ${DATABASE_URL}\"; exit 0")

(define-service svc-app "app"
  :image   "alpine:latest"
  :network "app-net"
  :command "/bin/sh" "-c"
           "echo \"db=${DATABASE_URL}\"; sleep 30; echo done")

;; ── Declare the chain — nothing executes yet ─────────────────────────────
;;
;; When (resolve ...) is called it walks upstream recursively:
;;
;;   1. resolve db              → ContainerHandle
;;   2. then: compute db-url    → "postgres://app:secret@<ip>/appdb"
;;   3. resolve migrate         → ContainerHandle  (:env wires DATABASE_URL)
;;   4. lambda returns app node → another node! resolve flattens it
;;   5. resolve app             → ContainerHandle  (:env wires DATABASE_URL)
;;
;; Step 4 is the monadic flatten: the lambda returns a *new node* rather
;; than a plain value.  resolve detects this and executes it automatically,
;; so the whole pipeline is a single (resolve pipeline) call.

(define db (start svc-db))

(define pipeline
  (then db
    (lambda (db-handle)
      (let ((db-url (format "postgres://app:secret@~a/appdb" (container-ip db-handle))))
        ;; Start migrate, injecting the db-url we just computed.
        (then (start svc-migrate
                :needs (list db)
                :env   (lambda (_) `(("DATABASE_URL" . ,db-url))))
          (lambda (_migrate)
            ;; migrate finished — now start app with the same db-url.
            ;; Returning a node here triggers the monadic flatten in resolve.
            (start svc-app
              :needs (list db)
              :env   (lambda (_) `(("DATABASE_URL" . ,db-url))))))))))

;; ── Execute: dynamic executor resolves the chain ──────────────────────────

(define app-handle (resolve pipeline))
(logf "app started: ~a" app-handle)

;; ── Wait and clean up ─────────────────────────────────────────────────────
;;
;; resolve does not populate ContainerHandle.deps, so there is no cascade
;; teardown here — we don't have handles to db or migrate at this scope.
;;
;; That's intentional: the chain model trades explicit handle access for a
;; simpler declaration style.  Cleanup is handled implicitly: when the
;; Interpreter drops at the end of this script (or on error), it sends
;; SIGTERM to all containers still in the registry — db, migrate (if still
;; running), and app — as a safety net.
;;
;; with-cleanup here handles logging only; no explicit container-stop needed.

(with-cleanup (lambda (result)
                (if (ok? result)
                  (logf "app exited cleanly (code ~a)" (ok-value result))
                  (logf "app failed: ~a" (err-reason result))))
  (container-wait app-handle))
(log "Done")
