;; 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.tool.core
  "Tool protocol and registry implementation.
   Layer 0: Pure functions for tool definitions and validation
   Layer 1: Registry management and tool execution"
  (:require
   [clojure.set :as set]
   [clojure.string :as str]
   [ai.miniforge.logging.interface :as log]
   [ai.miniforge.response.interface :as response]
   [ai.miniforge.tool.tracking :as tracking]))

;------------------------------------------------------------------------------ Layer 0
;; Tool schema and validation

(def tool-schema
  "Schema for tool definitions (Phase 0 - minimal implementation).

   Phase 0 provides core tool protocol for agent use.
   Phase 1+ will add MCP compatibility fields:
   - :tool/version (string) - semver
   - :tool/parameters (Malli schema) - instead of simple map
   - :tool/returns (Malli schema)
   - :tool/async? (boolean)
   - :tool/idempotent? (boolean)
   - :tool/side-effects (set of keywords)
   - :tool/cost-model (map)
   - :tool/risk-level (keyword)
   - :tool/timeout-ms (long)
   - :tool/requires (set of capability keywords)

   See docs/specs/extensibility.spec for full specification."
  {:tool/id          :keyword
   :tool/name        :string
   :tool/description :string
   :tool/parameters  :map       ; parameter spec {:param {:type :string :required true}}
   :tool/handler     :fn        ; (fn [params context] -> result)
   :tool/metadata    :map})     ; optional metadata (can include future MCP fields)

(defn valid-tool-id?
  "Check if tool ID is valid (namespaced keyword)."
  [id]
  (and (keyword? id)
       (namespace id)))

(defn validate-params
  "Validate parameters against a parameter spec.
   Returns {:valid? true} or {:valid? false :errors [...]}."
  [param-spec params]
  (let [required (set (keys (filter (fn [[_k v]] (:required v)) param-spec)))
        provided (set (keys params))
        missing (set/difference required provided)]
    (if (empty? missing)
      {:valid? true}
      {:valid? false
       :errors (mapv #(str "Missing required parameter: " (name %)) missing)})))

(defn build-tool-definition
  "Build a tool definition map from parts."
  [{:keys [id name description parameters handler metadata]
    :or {parameters {} metadata {}}}]
  {:tool/id id
   :tool/name (or name (clojure.core/name id))
   :tool/description description
   :tool/parameters parameters
   :tool/handler handler
   :tool/metadata metadata})

(defn tool-summary
  "Create a brief summary of a tool for display."
  [tool]
  (str (:tool/name tool) " - " (:tool/description tool)))

;------------------------------------------------------------------------------ Layer 1
;; Tool protocol and registry

(defprotocol Tool
  "Protocol for executable tools."
  (tool-id [this] "Return the tool's identifier.")
  (tool-info [this] "Return tool metadata and description.")
  (execute [this params context] "Execute the tool with given params and context.")
  (validate-args [this args] "Validate arguments against tool schema. Returns {:valid? bool :errors [...]}")
  (get-schema [this] "Return tool parameter schema."))

(defprotocol ToolRegistry
  "Protocol for tool registration and lookup."
  (register-tool [this tool] "Register a tool in the registry.")
  (unregister-tool [this tool-id] "Remove a tool from the registry.")
  (get-tool [this tool-id] "Retrieve a tool by ID.")
  (list-tools [this] "List all registered tools.")
  (find-tools [this query] "Find tools matching query."))

(defn emit-tool-event!
  "Emit a tool lifecycle event via requiring-resolve (no hard dep on event-stream)."
  [context tool-id event-type & [extra]]
  (try
    (when-let [stream (:event-stream context)]
      (when-let [publish-fn (requiring-resolve 'ai.miniforge.event-stream.interface/publish!)]
        (let [wf-id (:workflow/id context)
              agent-id (:agent/id context)
              constructor (case event-type
                            :invoked (requiring-resolve 'ai.miniforge.event-stream.interface/tool-invoked)
                            :completed (requiring-resolve 'ai.miniforge.event-stream.interface/tool-completed))]
          (publish-fn stream (constructor stream wf-id agent-id tool-id extra)))))
    (catch Exception _ nil)))

(defrecord FunctionTool [id name description parameters handler metadata]
  Tool
  (tool-id [_this] id)
  (tool-info [_this]
    {:id id
     :name name
     :description description
     :parameters parameters
     :metadata metadata})
  (execute [_this params context]
    (emit-tool-event! context id :invoked (select-keys params (take 5 (keys params))))
    (let [start-ms (System/currentTimeMillis)
          validation (validate-params parameters params)
          result (if (:valid? validation)
                   (try
                     {:success true
                      :result (handler params context)}
                     (catch Exception e
                       {:success false
                        :error {:type "execution_error"
                                :message (.getMessage e)}
                        :anomaly (response/from-exception e)}))
                   {:success false
                    :error {:type "validation_error"
                            :errors (:errors validation)}
                    :anomaly (response/make-anomaly
                              :anomalies/incorrect
                              (str "Tool parameter validation failed: "
                                   (first (:errors validation))))})
          end-ms (System/currentTimeMillis)
          invocation (tracking/build-invocation id params start-ms end-ms result)]
      (tracking/record-invocation context invocation)
      (emit-tool-event! context id :completed
                        (if (:success result)
                          {:success true}
                          {:success false :error-type (get-in result [:error :type])}))
      result))
  (validate-args [_this args]
    (validate-params parameters args))
  (get-schema [_this]
    parameters))

(defrecord AtomRegistry [tools-atom logger]
  ToolRegistry
  (register-tool [_this tool]
    (let [id (tool-id tool)]
      (swap! tools-atom assoc id tool)
      (when logger
        (log/debug logger :system :tool/registered
                   {:data {:tool-id id}}))
      id))

  (unregister-tool [_this tool-id]
    (swap! tools-atom dissoc tool-id)
    (when logger
      (log/debug logger :system :tool/unregistered
                 {:data {:tool-id tool-id}}))
    tool-id)

  (get-tool [_this tool-id]
    (get @tools-atom tool-id))

  (list-tools [_this]
    (vals @tools-atom))

  (find-tools [_this query]
    (let [q (str/lower-case (or query ""))]
      (->> @tools-atom
           vals
           (filter (fn [tool]
                     (let [info (tool-info tool)]
                       (or (str/includes? (str/lower-case (or (:name info) "")) q)
                           (str/includes? (str/lower-case (or (:description info) "")) q)))))))))

(defn create-function-tool
  "Create a function-based tool.

   Options:
   - :id          - Namespaced keyword identifying the tool (required)
   - :name        - Display name
   - :description - What the tool does
   - :parameters  - Map of param specs {:param {:type :string :required true}}
   - :handler     - (fn [params context] -> result)
   - :metadata    - Optional metadata map"
  [{:keys [id name description parameters handler metadata]
    :or {parameters {} metadata {}}}]
  (when-not (valid-tool-id? id)
    (throw (ex-info "Tool ID must be a namespaced keyword"
                    {:id id})))
  (->FunctionTool id
                  (or name (clojure.core/name id))
                  description
                  parameters
                  handler
                  metadata))

(defn create-registry
  "Create a new tool registry.

   Options:
   - :logger - Optional logger for registration events"
  ([] (create-registry {}))
  ([{:keys [logger]}]
   (->AtomRegistry (atom {}) logger)))

(defn execute-tool
  "Execute a tool from a registry by ID.
   Returns execution result or error if tool not found."
  [registry tool-id params context]
  (if-let [tool (get-tool registry tool-id)]
    (execute tool params context)
    {:success false
     :error {:type "not_found"
             :message (str "Tool not found: " tool-id)}
     :anomaly (response/make-anomaly
               :anomalies/not-found
               (str "Tool not found: " tool-id))}))

;------------------------------------------------------------------------------ Rich Comment
(comment
  ;; Create a simple tool
  (def echo-tool
    (create-function-tool
     {:id :tools/echo
      :name "Echo"
      :description "Returns the input unchanged"
      :parameters {:message {:type :string :required true}}
      :handler (fn [params _ctx] (:message params))}))

  ;; Create a registry and register the tool
  (def registry (create-registry))
  (register-tool registry echo-tool)

  ;; Execute the tool
  (execute-tool registry :tools/echo {:message "Hello!"} {})
  ;; => {:success true, :result "Hello!"}

  ;; List tools
  (map tool-info (list-tools registry))

  ;; Find tools
  (map tool-info (find-tools registry "echo"))

  ;; Validation failure
  (execute-tool registry :tools/echo {} {})
  ;; => {:success false, :error {:type "validation_error", ...}}

  :leave-this-here)
