;; 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.response.builder
  "Canonical builders for common response patterns.

   Eliminates scattered inline map constructions by providing
   standard builders for success/error responses with metrics."
  (:require [clojure.string]))

;------------------------------------------------------------------------------ Layer 0
;; Success responses

(defn success
  "Create a canonical success response.

   Arguments:
   - output: The main result data
   - opts: Optional map with :metrics, :artifact, :tokens, :duration-ms

   Returns:
   {:status :success
    :output output
    :artifact artifact-or-output
    :metrics {:tokens N :duration-ms N}}

   Examples:
     (success test-artifact {:tokens 100 :duration-ms 500})
     (success code-data {:metrics {:tokens 200} :artifact processed-code})"
  ([output]
   (success output {}))
  ([output {:keys [metrics artifact tokens duration-ms]}]
   (let [default-metrics {:tokens (or tokens 0)
                         :duration-ms (or duration-ms 0)}
         merged-metrics (merge default-metrics metrics)]
     (cond-> {:status :success
              :output output
              :metrics merged-metrics}
       artifact (assoc :artifact artifact)
       (not artifact) (assoc :artifact output)))))

(defn success?
  "True for response maps produced by `success` (have :status :success).

   Use this on single result maps. For response *chains* (have :succeeded? key),
   use `succeeded?` instead."
  [response]
  (= :success (:status response)))

;------------------------------------------------------------------------------ Layer 1
;; Error responses

(defn- format-stack-frame
  "Render one StackTraceElement as ClassName.methodName (File:line)."
  [^StackTraceElement e]
  (str (.getClassName e) "." (.getMethodName e)
       " (" (.getFileName e) ":" (.getLineNumber e) ")"))

(defn- stack-preview
  "Render the top N frames of a stack trace as short strings. Returns
   nil when the exception has no stack (e.g. doto-constructed Throwable)."
  [^Throwable ex n]
  (when-let [st (seq (.getStackTrace ex))]
    (mapv format-stack-frame (take n st))))

(defn- exception->data
  "Exception → :data map with :exception/class and (when available)
   :exception/stack-preview. Kept short so failures like NPE — which
   carry no getMessage or ex-data — still name the exception class
   and top frames in the phase-completed event."
  [^Throwable ex]
  (let [preview (stack-preview ex 8)]
    (cond-> {:exception/class (.getName (class ex))}
      preview (assoc :exception/stack-preview preview))))

(defn error-details
  "Create canonical error details map.

   Arguments:
   - message: Error message string or Exception
   - data: Optional error data map

   Returns:
   {:message string :data map}

   Examples:
     (error-details \"Validation failed\" {:field :email})
     (error-details ex (ex-data ex))"
  ([message]
   (error-details message nil))
  ([message data]
   (let [exception? (instance? Exception message)
         raw-msg (if exception?
                   (.getMessage ^Exception message)
                   (str message))
         ;; Ensure message is never nil or empty — downstream (or msg fallback)
         ;; treats "" as truthy, masking useful defaults
         msg (if (or (nil? raw-msg) (clojure.string/blank? raw-msg))
               (if exception?
                 (str (class message))
                 "Unknown error")
               raw-msg)
         ;; For Exception messages, attach class + optional stack preview
         ;; so failures like NPE (no getMessage, no ex-data) don't come
         ;; through as {:message "Unknown error" :data {}} — that empty
         ;; shape is what made iter-11 planner failure un-diagnosable.
         exception-data (when exception?
                          (exception->data message))
         err-data (merge (when exception? exception-data)
                         (when exception? (ex-data message))
                         data)]
     {:message msg
      :data (or err-data {})})))

(defn error
  "Create a canonical error response.

   Arguments:
   - message: Error message string or Exception
   - opts: Optional map with :data, :metrics, :output, :tokens, :duration-ms

   Returns:
   {:status :error
    :error {:message string :data map}
    :metrics {:tokens N :duration-ms N}
    :output output-if-provided}

   Examples:
     (error \"Test failed\" {:tokens 50 :duration-ms 200})
     (error ex {:data (ex-data ex) :metrics {:tokens 100}})"
  ([message]
   (error message {}))
  ([message {:keys [data metrics output tokens duration-ms]}]
   (let [default-metrics {:tokens (or tokens 0)
                         :duration-ms (or duration-ms 0)}
         merged-metrics (merge default-metrics metrics)]
     (cond-> {:status :error
              :error (error-details message data)
              :metrics merged-metrics}
       output (assoc :output output)))))

(defn error?
  "True for response maps representing failure.

   Matches:
   - `:status :error`  (output of `error`)
   - `:status :failed` (legacy/phase shape)
   - `:success false`  (output of `failure`)

   Use this on single result maps. For response *chains*, use `failed?`."
  [response]
  (let [status (:status response)]
    (or (= :error status)
        (= :failed status)
        (false? (:success response)))))

(defn failure
  "Create a canonical failure response (alias for error with :success false).

   Same as error but uses :success false instead of :status :error
   for backward compatibility.

   Examples:
     (failure \"Agent timeout\" {:tokens 0 :duration-ms 5000})"
  ([message]
   (failure message {}))
  ([message opts]
   (-> (error message opts)
       (dissoc :status)
       (assoc :success false))))

;------------------------------------------------------------------------------ Layer 2
;; Status checks (timestamped observations)

(defn status-check
  "Create a timestamped status check response.

   Use for point-in-time observations like health checks, monitoring, polling.
   Always includes :checked-at timestamp. For immediate operation results without
   timestamps, use success/error instead.

   Arguments:
   - status - Status keyword (:healthy, :warning, :halt, or custom)
   - opts - Optional map with :message, :data, :agent/id, or other context

   Returns:
   {:status status
    :checked-at java.time.Instant
    ...opts}

   Examples:
     (status-check :healthy
                   {:agent/id :progress-monitor
                    :message \"Workflow active: 10 chunks\"
                    :data {:chunks 10}})

     (status-check :halt
                   {:halting-agent :test-quality
                    :halt-reason \"Coverage too low\"
                    :checks [...]})"
  [status opts]
  (merge {:status status
          :checked-at (java.time.Instant/now)}
         opts))

;------------------------------------------------------------------------------ Layer 3
;; Validation errors

(defn validation-error
  "Create validation error entry.

   Arguments:
   - error: Error string or keyword

   Returns:
   String for error message

   Examples:
     (validation-error :missing-field)
     (validation-error \"Invalid email format\")"
  [error]
  (if (keyword? error)
    (name error)
    (str error)))

(defn validation-result
  "Create validation result map.

   Arguments:
   - errors: Vector of error strings (empty = valid)

   Returns:
   {:valid? boolean :errors vector}

   Examples:
     (validation-result [])
     (validation-result [\"Missing field\" \"Invalid format\"])"
  [errors]
  (if (empty? errors)
    {:valid? true}
    {:valid? false
     :errors (vec errors)}))

;------------------------------------------------------------------------------ Rich Comment
(comment
  ;; Success responses
  (success {:test/id 123} {:tokens 100 :duration-ms 500})
  ;; => {:status :success
  ;;     :output {:test/id 123}
  ;;     :artifact {:test/id 123}
  ;;     :metrics {:tokens 100 :duration-ms 500}}

  ;; Error responses
  (error "Failed to compile" {:tokens 50 :duration-ms 200})
  ;; => {:status :error
  ;;     :error {:message "Failed to compile" :data {}}
  ;;     :metrics {:tokens 50 :duration-ms 200}}

  (error (ex-info "Timeout" {:phase :verify}))
  ;; => {:status :error
  ;;     :error {:message "Timeout" :data {:phase :verify}}
  ;;     :metrics {:tokens 0 :duration-ms 0}}

  ;; Validation
  (validation-result [])
  ;; => {:valid? true}

  (validation-result ["Missing field" "Invalid email"])
  ;; => {:valid? false :errors ["Missing field" "Invalid email"]}

  ;; Status checks (timestamped)
  (status-check :healthy {:agent/id :progress-monitor
                          :message "Making progress"
                          :data {:chunks 5}})
  ;; => {:status :healthy
  ;;     :checked-at #inst "2026-01-31T..."
  ;;     :agent/id :progress-monitor
  ;;     :message "Making progress"
  ;;     :data {:chunks 5}}

  :end)
