;; 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.dag-primitives.result
  "Result monad — consistent ok/err patterns for pipeline and DAG operations."
  (:require [ai.miniforge.anomaly.interface :as anomaly]))

;;------------------------------------------------------------------------------ Layer 0
;; Constructors

(defn ok
  "Success result.  Returns {:ok? true :data data}"
  [data]
  {:ok? true :data data})

(defn err
  "Error result.  Returns {:ok? false :error {:code kw :message str [:data map]}}"
  ([code message]
   {:ok? false :error {:code code :message message}})
  ([code message data]
   {:ok? false :error {:code code :message message :data data}}))

(defn ok?  [result] (:ok? result false))
(defn err? [result] (not (:ok? result true)))

;;------------------------------------------------------------------------------ Layer 1
;; Extraction

(defn unwrap-anomaly
  "Extract data from a result. On ok, return the wrapped data; on err,
   return a canonical `:fault` anomaly carrying the error code, message,
   and any error data the result carried.

   Prefer this over [[unwrap]] in non-boundary code: callers can fold the
   anomaly into a response chain or pattern-match on `:anomaly/type`
   rather than wrapping every call in try/catch."
  [result]
  (if (ok? result)
    (:data result)
    (let [error (:error result)]
      (anomaly/anomaly :fault
                       (or (:message error) "Unwrap called on error result")
                       (cond-> {}
                         (:code error) (assoc :code (:code error))
                         (:data error) (assoc :error-data (:data error)))))))

(defn unwrap
  "Extract data from an ok result; throw on error.

   DEPRECATED: prefer [[unwrap-anomaly]], which returns an anomaly map
   rather than throwing. This thrower is retained for backward compatibility
   with existing callsites; new code should consume `unwrap-anomaly` and
   fold its result into a response chain."
  {:deprecated "exceptions-as-data — prefer unwrap-anomaly"}
  [result]
  (if (ok? result)
    (:data result)
    (throw (ex-info "Unwrap called on error result" {:error (:error result)}))))

(defn unwrap-or
  "Extract data from an ok result, or return default on error."
  [result default]
  (if (ok? result) (:data result) default))

;;------------------------------------------------------------------------------ Layer 2
;; Transforms

(defn map-ok
  "Apply f to the data inside an ok result; pass errors through."
  [result f]
  (if (ok? result) (ok (f (:data result))) result))

(defn map-err
  "Apply f to the error inside an err result; pass oks through."
  [result f]
  (if (err? result) {:ok? false :error (f (:error result))} result))

;;------------------------------------------------------------------------------ Layer 3
;; Combinators

(defn and-then
  "Chain f (returns a result) onto an ok result; pass errors through."
  [result f]
  (if (ok? result) (f (:data result)) result))

(defn or-else
  "Chain f (returns a result) onto an err result; pass oks through."
  [result f]
  (if (err? result) (f (:error result)) result))

(defn collect
  "Collect a seq of results into a result of seq.
   Returns the first error encountered, or ok of all data values."
  [results]
  (loop [remaining results
         collected []]
    (if (empty? remaining)
      (ok collected)
      (let [r (first remaining)]
        (if (ok? r)
          (recur (rest remaining) (conj collected (:data r)))
          r)))))
