;; 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.cli.workflow-runner.context
  "Workflow input resolution and runtime context creation."
  (:require
   [clojure.edn :as edn]
   [clojure.java.shell :as shell]
   [clojure.string :as str]
   [babashka.fs :as fs]
   [cheshire.core :as json]
   [ai.miniforge.cli.config :as config]
   [ai.miniforge.cli.worktree :as worktree]
   [ai.miniforge.cli.workflow-runner.display :as display]
   [ai.miniforge.event-stream.interface :as es]
   [ai.miniforge.response.interface :as response]))

;------------------------------------------------------------------------------ Layer 0
;; Input resolution

(defn read-input-file [path]
  (when path
    (let [file (fs/file path)]
      (when-not (fs/exists? file)
        (response/throw-anomaly! :anomalies/not-found
                                (str "Input file not found: " path)
                                {:path path}))
      (let [content (slurp file)
            ext (fs/extension file)]
        (case ext
          "edn" (edn/read-string content)
          "json" (json/parse-string content true)
          (response/throw-anomaly! :anomalies/unsupported
                                  (str "Unsupported file format: " ext " (use .edn or .json)")
                                  {:path path :extension ext}))))))

(defn parse-inline-json [s]
  (when s
    (try
      (json/parse-string s true)
      (catch Exception e
        (response/throw-anomaly! :anomalies/fault
                                (str "Failed to parse input JSON: " (ex-message e))
                                {:input s})))))

(defn resolve-input [{:keys [input input-json]}]
  (cond
    input-json (parse-inline-json input-json)
    input (read-input-file input)
    :else {}))

(defn- resolve-git-field
  [dir & args]
  (let [{:keys [exit out]} (apply shell/sh (concat ["git"] args [:dir dir]))]
    (when (zero? exit)
      (some-> out str/trim not-empty))))

(defn- resolve-git-bool
  [dir & args]
  (boolean (apply resolve-git-field dir args)))

(defn get-git-info
  ([] (get-git-info nil))
  ([start-path]
   (try
     (when-let [root (worktree/worktree-root start-path)]
       (let [branch (resolve-git-field root "rev-parse" "--abbrev-ref" "HEAD")
             commit (resolve-git-field root "rev-parse" "--short" "HEAD")]
         (when (and branch commit)
           {:git-branch branch
            :git-commit commit})))
     (catch Exception _ nil))))

(defn get-git-state
  ([] (get-git-state nil))
  ([start-path]
   (try
     (when-let [root (worktree/worktree-root start-path)]
       (let [branch (resolve-git-field root "rev-parse" "--abbrev-ref" "HEAD")
             commit (resolve-git-field root "rev-parse" "--short" "HEAD")
             upstream (resolve-git-field root "rev-parse" "--abbrev-ref" "--symbolic-full-name" "@{upstream}")
             dirty? (resolve-git-bool root "status" "--porcelain")]
         (when (and branch commit)
           {:git-branch branch
            :git-commit commit
            :git-upstream upstream
            :git-dirty? dirty?
            :git-detached? (= "HEAD" branch)})))
     (catch Exception _ nil))))

(defn- execution-worktree-path
  [execution-opts]
  (get execution-opts :worktree-path))

(defn- source-root-path
  [source-dir]
  (or (worktree/worktree-root source-dir)
      source-dir
      (worktree/worktree-root)
      (System/getProperty "user.dir")))

(defn get-files-in-scope
  "Resolve scope paths to actual file paths.

   Handles both individual files and directories. Directories are expanded
   to their contained source files (*.clj, *.cljc, *.cljs, *.edn)."
  [intent]
  (->> (get intent :scope [])
       (mapcat (fn [path]
                 (try
                   (cond
                     (not (fs/exists? path))
                     [path] ;; Keep non-existent paths for error reporting

                     (fs/directory? path)
                     (->> (fs/glob path "**.{clj,cljc,cljs,edn}")
                          (map str)
                          vec)

                     :else [path])
                   (catch Exception _ [path]))))
       vec))

(defn spec->workflow-input [enriched-spec]
  (merge (:spec/raw-data enriched-spec)
         {:title (:spec/title enriched-spec)
          :description (:spec/description enriched-spec)
          :intent (:spec/intent enriched-spec)
          :constraints (:spec/constraints enriched-spec)
          :acceptance-criteria (:spec/acceptance-criteria enriched-spec)
          :code-artifact (:spec/code-artifact enriched-spec)
          :plan-tasks (:spec/plan-tasks enriched-spec)
          :repo-url (:spec/repo-url enriched-spec)
          :branch (:spec/branch enriched-spec)
          :llm-backend (:spec/llm-backend enriched-spec)
          :sandbox (:spec/sandbox enriched-spec)
          :context (:spec/context enriched-spec)
          :metadata (:spec/metadata enriched-spec)
          :provenance (:spec/provenance enriched-spec)}))

;------------------------------------------------------------------------------ Layer 1
;; Context decoration

(defn decorate-spec-with-runtime-context [spec {:keys [iteration parent-task-id] :or {iteration 1}}]
  (let [cwd (or (worktree/worktree-root) (str (fs/cwd)))
        git-info (get-git-info cwd)
        files-in-scope (get-files-in-scope (:spec/intent spec))]
    (assoc spec
           :spec/context
           (cond-> {:cwd cwd
                    :files-in-scope files-in-scope
                    :environment :development}
             git-info (merge git-info))

           :spec/metadata
           (cond-> {:submitted-at (java.util.Date.)
                    :session-id (random-uuid)
                    :iteration iteration}
             parent-task-id (assoc :parent-task-id parent-task-id)))))

(defn create-llm-client
  ([workflow spec quiet] (create-llm-client workflow spec quiet nil))
  ([workflow spec quiet backend-override]
   (try
     (let [cfg (config/load-config)
           llm-backend (config/get-llm-backend
                        cfg
                        (or backend-override
                            (get-in workflow [:workflow/config :llm-backend])
                            (:spec/llm-backend spec)))]
       (when-let [create-client (requiring-resolve 'ai.miniforge.llm.interface/create-client)]
         (create-client {:backend llm-backend})))
     (catch Exception e
       (when-not quiet
         (println (display/colorize :yellow (str "Warning: Could not create LLM client (" (ex-message e) "), agents will use fallback mode"))))
       nil))))

;------------------------------------------------------------------------------ Layer 2
;; Workflow context assembly

(defn create-workflow-context [{:keys [callbacks artifact-store event-stream workflow-id
                                       workflow-type workflow-version llm-client quiet
                                       spec-title control-state skip-lifecycle-events
                                       execution-opts source-dir
                                       routing-trigger-event-id]}]
  (let [on-chunk (es/create-streaming-callback event-stream workflow-id :agent
                                               {:print? (not quiet) :quiet? quiet})
        source-root (source-root-path source-dir)
        git-info (get-git-state source-root)
        worktree-path (or (execution-worktree-path execution-opts)
                          (worktree/worktree-root)
                          (System/getProperty "user.dir"))]
    (es/publish! event-stream
                 (es/workflow-started event-stream workflow-id
                                      {:name (or spec-title (name workflow-type))
                                       :version workflow-version}
                                      ;; N15-4: thread the routing-trigger-event-id when the
                                      ;; caller (an external listener invoking the CLI, a
                                      ;; primer-driven resume, an in-process dispatcher) named
                                      ;; the routing trigger that fired this workflow.
                                      {:routing/trigger-event-id routing-trigger-event-id}))
    (cond-> callbacks
      llm-client (assoc :llm-backend llm-client)
      artifact-store (assoc :artifact-store artifact-store)
      on-chunk (assoc :on-chunk on-chunk)
      event-stream (assoc :event-stream event-stream)
      control-state (assoc :control-state control-state)
      skip-lifecycle-events (assoc :skip-lifecycle-events true)
      execution-opts (assoc :execution/opts execution-opts)
      source-root (assoc :source-root source-root)
      git-info (merge git-info)
      true (assoc :worktree-path worktree-path))))
