;; Title: Miniforge.ai
;; Subtitle: An agentic SDLC / fleet-control platform
;; Author: Christopher Lester
;; Line: Founder, Miniforge.ai (project)
;; Copyright 2025-2026 Christopher Lester (christopher@miniforge.ai)
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;;     http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.

(ns ai.miniforge.agent.meta-protocol
  "Protocol and interface for meta-agents that monitor and control workflows.

   Meta-agents are specialized agents that observe workflow execution and can
   halt the factory floor when they detect issues. Unlike regular agents that
   produce artifacts, meta-agents produce decisions about workflow health.

   Design Philosophy:
   - Any meta-agent can stop the workflow (distributed authority)
   - Each meta-agent has focused domain expertise
   - Decisions are observable and logged
   - Lightweight coordinator routes information, doesn't control"
  (:require [ai.miniforge.anomaly.interface :as anomaly]
            [ai.miniforge.response.interface :as response]
            [clojure.string :as str]))

(defprotocol MetaAgent
  "Protocol for meta-agents that monitor workflow execution.

   Meta-agents receive workflow state updates and decide whether
   the workflow should continue or halt."

  (check-health [this workflow-state]
    "Check workflow health and return a decision.

     Arguments:
     - workflow-state: Current workflow state map including:
       {:workflow/id UUID
        :workflow/phase :plan|:implement|:verify|:review|:release
        :workflow/iterations number
        :workflow/artifacts {...}
        :workflow/metrics {...}
        :workflow/streaming-activity [...]
        :workflow/files-written [...]}

     Returns:
     {:status :healthy|:warning|:halt|:intervene
      :agent/id keyword (e.g., :progress-monitor)
      :message string (human-readable explanation)
      :data {} (diagnostic data)
      :checked-at inst
      ;; Only when :status is :intervene:
      :intervention/addendum string  ; text appended to next agent invocation
      :intervention/scope    keyword ; :next-implement | :next-N-phases | :workflow
      :intervention/n        int     ; for :next-N-phases scope}

     Status meanings:
     - :healthy   - Workflow is progressing normally
     - :warning   - Issue detected but not critical yet
     - :halt      - Workflow must stop immediately
     - :intervene - Workflow may continue but must apply the supplied
                    instruction addendum to the next agent invocation
                    in scope. This is the soft-fix path: an alternative
                    to :halt when the meta-agent has a specific fix in
                    mind (e.g. \"implementer keeps using reify on
                    abstract class — tell it to use proxy instead\").
                    See work/meta-agent-repair-loop-intervention.spec.edn.")

  (get-meta-config [this]
    "Get meta-agent configuration.

     Returns:
     {:agent/id :progress-monitor
      :agent/name \"Progress Monitor\"
      :agent/can-halt? true
      :agent/check-interval-ms 30000
      :agent/priority :high|:medium|:low}")

  (reset-state! [this]
    "Reset meta-agent internal state. Called when workflow restarts."))

(defn healthy?
  "Check if health status indicates workflow can continue."
  [health-check]
  (= :healthy (:status health-check)))

(defn halt?
  "Check if health status indicates workflow must stop."
  [health-check]
  (= :halt (:status health-check)))

(defn warning?
  "Check if health status indicates a warning."
  [health-check]
  (= :warning (:status health-check)))

(def ^:private intervention-scopes
  "Valid scopes for an :intervene decision. The runner consults the
   scope to decide how many agent invocations the addendum applies to:
     - :next-implement → exactly the next implement call, one-shot
     - :next-N-phases  → next N phase invocations, regardless of phase id
     - :workflow       → every remaining agent call in this workflow run"
  #{:next-implement :next-N-phases :workflow})

(defn intervene?
  "Check if health status indicates the workflow should continue with
   an injected instruction addendum applied to the next in-scope agent
   invocation."
  [health-check]
  (= :intervene (:status health-check)))

(defn valid-intervention?
  "True when a health-check carrying `:status :intervene` has the
   companion intervention keys populated to a usable shape:

   - `:intervention/addendum` is a non-blank string.
   - `:intervention/scope` is one of `intervention-scopes`.
   - For `:next-N-phases` scope, `:intervention/n` is a positive int.

   Workflow runners MUST validate before applying — an :intervene
   decision with a missing or malformed addendum is treated as a
   :warning, not an unhandled exception."
  [health-check]
  (let [{:intervention/keys [addendum scope n]} health-check]
    (and (intervene? health-check)
         (string? addendum)
         (not (str/blank? addendum))
         (contains? intervention-scopes scope)
         (if (= :next-N-phases scope)
           (and (integer? n) (pos? n))
           true))))

(defn create-health-check
  "Helper to create a health check result.

   Delegates to response/status-check for consistent timestamped responses.

   Usage:
   (create-health-check :progress-monitor :healthy \"Making progress\")
   (create-health-check :test-quality :halt \"No tests found\" {:files-checked 10})"
  ([agent-id status message]
   (create-health-check agent-id status message nil))
  ([agent-id status message data]
   (response/status-check status
                          (cond-> {:agent/id agent-id
                                   :message message}
                            data (assoc :data data)))))

(defn create-intervention
  "Helper to create an `:intervene` health-check result.

   Returns a health-check map with `:status :intervene` plus the
   companion `:intervention/*` keys. Use this in preference to
   hand-building the map — keeps the shape canonical and the validation
   contract (`valid-intervention?`) in sync.

   Required:
   - `agent-id` — the meta-agent's `:agent/id` keyword.
   - `message`  — short human-readable why (shown in observability).
   - `addendum` — non-blank string appended to the next in-scope
                  agent invocation's task description.

   Optional (defaults):
   - `:scope`   — `:next-implement` (default) | `:next-N-phases` | `:workflow`.
   - `:n`       — required when `scope` is `:next-N-phases`; ignored
                  otherwise.
   - `:data`    — additional diagnostic data the meta-agent wants to
                  surface alongside the intervention (e.g. the
                  fingerprint history that triggered it)."
  [agent-id message addendum
   & {:keys [scope n data]
      :or   {scope :next-implement}}]
  ;; Fail loud when :next-N-phases is requested without a positive :n.
  ;; The previous shape silently assoc'd :intervention/n nil, producing
  ;; an intervention that immediately failed valid-intervention? at
  ;; the runner — confusing because it looked syntactically fine but
  ;; was semantically malformed. Surface the misconfiguration here.
  (when (and (= :next-N-phases scope)
             (not (and (integer? n) (pos? n))))
    (throw (ex-info "create-intervention: :next-N-phases scope requires positive integer :n"
                    {:scope     scope
                     :n         n
                     :hint      "either pass :n <pos-int>, or pick a scope that doesn't need it (:next-implement or :workflow)"})))
  (let [base (response/status-check :intervene
                                    (cond-> {:agent/id agent-id
                                             :message message}
                                      data (assoc :data data)))]
    (cond-> (assoc base
                   :intervention/addendum addendum
                   :intervention/scope    scope)
      (= :next-N-phases scope) (assoc :intervention/n n))))

(defrecord MetaAgentConfig
  [id                    ; Keyword ID (:progress-monitor, :test-quality, etc.)
   name                  ; Human-readable name
   can-halt?             ; Can this agent halt the workflow?
   check-interval-ms     ; How often to run health checks
   priority              ; :high, :medium, :low - affects check ordering
   enabled?])            ; Is this meta-agent active?

(defn create-meta-config
  "Create a meta-agent configuration.

   This is the canonical, anomaly-returning constructor. Returns an
   `:invalid-input` anomaly when required fields are missing instead
   of throwing — callers branch on `anomaly/anomaly?` and decide
   whether to short-circuit or escalate.

   Options:
   - :id (required) - Keyword identifier
   - :name (required) - Human-readable name
   - :can-halt? (default: true) - Can halt workflow
   - :check-interval-ms (default: 30000) - Check frequency
   - :priority (default: :medium) - :high, :medium, :low
   - :enabled? (default: true) - Is active

   Returns:
   - a `MetaAgentConfig` record on success
   - an `:invalid-input` anomaly carrying `:provided` (the original
     opts map) when `:id` or `:name` is missing"
  [{:keys [id name can-halt? check-interval-ms priority enabled?]
    :or {can-halt? true
         check-interval-ms 30000
         priority :medium
         enabled? true}
    :as opts}]
  (if-not (and id name)
    (anomaly/anomaly :invalid-input
                     "Meta-agent config requires :id and :name"
                     {:provided opts})
    (map->MetaAgentConfig
     {:id id
      :name name
      :can-halt? can-halt?
      :check-interval-ms check-interval-ms
      :priority priority
      :enabled? enabled?})))

(comment
  ;; Example meta-agent implementation
  #_(defrecord ProgressMonitorAgent [monitor-state]
    MetaAgent
    (check-health [this workflow-state]
      (let [timeout-check (check-timeout @monitor-state)]
        (if timeout-check
          (create-health-check
           :progress-monitor
           :halt
           (:message timeout-check)
           (:stats timeout-check))
          (create-health-check
           :progress-monitor
           :healthy
           "Workflow making progress"
           (get-stats @monitor-state)))))

    (get-meta-config [this]
      (create-meta-config
       {:id :progress-monitor
        :name "Progress Monitor"
        :can-halt? true
        :check-interval-ms 30000
        :priority :high}))

    (reset-state! [this]
      (reset! monitor-state (create-progress-monitor {})))))
