# ═══════════════════════════════════════════════════════════════
# FlowMarkup Example — Host Application Services
# ═══════════════════════════════════════════════════════════════
# Demonstrates the two-way connectivity between a FlowMarkup flow
# and the Java application that embeds the engine.
#
# SERVICE ORIGINS — two kinds, used side-by-side:
#
#   ENGINE-PROVISIONED (host registers at engine startup):
#     SERVICES.cms      — host document store
#     SERVICES.search   — host search indexer
#     SERVICES.notify   — host push notification service
#
#   FLOW-DEFINED (declared in services: block below):
#     SERVICES.db       — PostgreSQL audit log  (progralink built-in)
#     SERVICES.cache    — Redis dedup guard      (progralink built-in)
#
# The flow has no knowledge of host internals — only the operation
# contract matters. Retry, circuit breaker, and timeout decorate
# every call uniformly, regardless of which kind of service it is.
# ═══════════════════════════════════════════════════════════════

flowmarkup:
  title: Host Application Services
  version: 1
  documentation: |
    Processes a document publish event end-to-end: loads and publishes
    the document via the host CMS service, writes an audit log to
    flow-defined PostgreSQL, guards against double-publishing via Redis,
    then fans out to the host search indexer and push notification
    service in parallel.

    The host Java application registers cms, search, and notify at
    engine startup (FlowMarkupEngine.registerService). The flow
    declares what it needs in requires.SERVICES — the engine enforces
    this as a capability grant before the flow starts.

  # ── Engine-provisioned services (host registers at startup) ──────────────
  # The host calls FlowMarkupEngine.registerService("cms", new DocumentService(...))
  # at boot. The flow declares what protocols and operations it expects.
  # Mismatches (wrong provider, missing operation) are caught at load time.
  requires:
    SERVICES:
      cms:
        provider: com.example.cms.DocumentService
        operations: [load, publish, archive, setMetadata]
        _notes_: Host document store — resolved by the embedding CMS application
      search:
        provider: com.example.search.IndexService
        operations: [index, remove]
        _notes_: Elasticsearch indexer — registered by host at engine startup
      notify:
        provider: com.example.notifications.PushService
        operations: [broadcast]
        _notes_: Push notification service — uses host-managed subscriber registry
    ENV: [AUDIT_DB_HOST, REDIS_HOST]
    SECRET: [audit_db_user, audit_db_password, redis_password]

  # ── Flow-defined services (engine instantiates these) ────────────────────
  # No host-side registration needed. The engine resolves the provider ID
  # to its built-in implementation and creates one instance per flow execution.
  # Properties are CEL-evaluated at lazy init (first call), so secret rotation
  # is picked up between executions without redeploying the flow.
  services:
    db:
      provider: progralink.clients.db.postgres
      _notes_: Audit log database — credentials from vault
      properties:
        host: =ENV.AUDIT_DB_HOST
        port: 5432
        database: audit_log
        username: =SECRET.audit_db_user
        password: =SECRET.audit_db_password
        max-pool-size: 5
    cache:
      provider: progralink.clients.cache.redis
      _notes_: Deduplication guard — prevents double-publishing on retry
      properties:
        host: =ENV.REDIS_HOST
        port: 6379
        password: =SECRET.redis_password

  input:
    doc_id: STRING
    requested_by: STRING
    notify_subscribers: {$default: true}
    index_immediately: {$default: true}

  output:
    published_at: STRING
    indexed: BOOLEAN
    notified_count: INTEGER
  throws:
  - DocumentNotFoundError
  - AlreadyPublishedError
  - PublishError
  vars:
    document:
    published_at:
    indexed: false
    notified_count: 0
    dedup_key:

  do:

    # ── 1. Dedup guard via Redis (flow-defined service) ───────────────────
    # Redis is declared in services: above — a progralink built-in provider.
    # No host code needed; the engine creates and manages the connection.
  - set:
      dedup_key: "='publish:' + doc_id"

  - call:
      _label_: Check dedup key
      service: cache
      operation: get
      params:
        key: =dedup_key
      result:
        cached_publish_time: =RESULT.value

  - throw:
      condition: =cached_publish_time != null
      error: AlreadyPublishedError
      message: "Document {{doc_id}} was already published at {{cached_publish_time}}"
  - call:
      _label_: Load document
      service: cms
      operation: load
      params:
        id: =doc_id
      timeout: 10s
      result: [document]
      errors: [DocumentNotFoundError]
  - log: "CMS: {{SERVICES.cms.meta.type}} {{SERVICES.cms.meta.version}} | DB: {{SERVICES.db.meta.type}}"

    # ── 4. Publish via host CMS service, with full resilience stack ────────
    # Retry, circuit breaker, and timeout apply uniformly to both host-
    # provided and flow-defined services — the engine owns the call stack.
  - call:
      _label_: Publish document
      service: cms
      operation: publish
      params:
        id: =doc_id
        published_by: =requested_by
      retry:
        maxAttempts: 3
        delay: 1s
        backoff: EXPONENTIAL
        nonRetryable: [DocumentNotFoundError, PublishError]
      circuitBreaker:
        name: cms.publish
        threshold: 5
        window: 1m
        resetTimeout: 30s
        scope: GLOBAL
      timeout: 15s
      result: [published_at]
      errors: [PublishError]
  - call:
      service: cache
      operation: set
      params:
        key: =dedup_key
        value: =published_at
        ttl: 86400s
      async: true

    # ── 6. Write audit log — PostgreSQL (flow-defined service) ─────────────
  - call:
      _label_: Write audit log
      service: db
      operation: execute
      params:
        sql: INSERT INTO publish_log (doc_id, published_by, published_at) VALUES (?, ?, ?)
        params: [=doc_id, =requested_by, =published_at]
  - parallel:
      _label_: Post-publish fanout
      failPolicy: COMPLETE
      indexing:
      - call:
          condition: =document != null && index_immediately
          _label_: Index document
          service: search
          operation: index
          params:
            id: =doc_id
            content: =document.content
            metadata: =document.metadata
          retry: 2/2s
          timeout: 10s
          result:
            indexed: =RESULT.success
      notify:
      - call:
          condition: =document != null && notify_subscribers
          _label_: Notify subscribers
          service: notify
          operation: broadcast
          params:
            topic: "='document.published.' + document.category"
            payload:
              doc_id: =doc_id
              title: =document.title
              published_at: =published_at
          timeout: 8s
          result:
            notified_count: =RESULT.recipient_count

  - return: [published_at, indexed, notified_count]
