;;; Graph model compose example
;;;
;;; The dependency graph is declared as first-class values before any
;;; execution begins.  `run` topologically sorts and executes the graph;
;;; pass :parallel to run independent nodes concurrently within each tier.
;;;
;;; Dependency graph:
;;;
;;;   db ──→ db-url ──→ migrate ──→ app
;;;   db ──→ db-url ─────────────→ app  (DATABASE_URL env)
;;;   cache ──→ cache-url ─────────→ app
;;;
;;; Usage:
;;;   sudo -E pelagos compose up -f compose.reml -p imperative-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-cache "cache"
  :image   "redis:7-alpine"
  :network "app-net")

(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} cache=${CACHE_URL}\"; sleep 5; echo done")

;; ── Declare the graph — nothing executes yet ─────────────────────────────

;; Independent services: no dependencies, eligible to start in parallel
(define-nodes
  (db    svc-db)
  (cache svc-cache))

;; Compute connection strings once each container is up
(define-then db-url db (h)
  (format "postgres://app:secret@~a/appdb" (container-ip h)))

(define-then cache-url cache (h)
  (format "redis://~a:6379" (container-ip h)))

;; Migration: needs db-url injected into its environment
(define migrate (start svc-migrate
  :needs (list db-url)
  :env   (lambda (url) `(("DATABASE_URL" . ,url)))))

;; Application: waits for migration to complete, then starts with both URLs
(define app (start svc-app
  :needs (list migrate db-url cache-url)
  :env   (lambda (_ db-url cache-url)
           `(("DATABASE_URL" . ,db-url)
             ("CACHE_URL"    . ,cache-url)))))

;; ── Execute and bind ──────────────────────────────────────────────────────

;; Only bind what you need.  Containers (app) resolve to handles; transforms
;; (db-url, cache-url) resolve to their computed values — both work the same way.
;; db and cache execute as transitive deps and stop automatically via the cascade;
;; add e.g. (db-handle db) here to inspect or manually stop them.
(define-run :parallel
  (app-handle app)
  (db-url     db-url)
  (cache-url  cache-url))

(logf "db-url:    ~a" db-url)
(logf "cache-url: ~a" cache-url)

;; cascade stops migrate, cache, db automatically in reverse startup order
(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")
