;; 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.compliance-scanner.execute
  "Apply auto-fixable violations to files and create one PR per rule.

   Layer 0: File patch helpers (pure)
   Layer 1: Git shell helpers
   Layer 1.5: PR documentation (pure)
   Layer 2: Per-rule fix, commit, and PR
   Layer 3: Top-level execute! entry point"
  (:require [clojure.java.io    :as io]
            [clojure.java.shell :as shell]
            [clojure.string     :as str]))

;------------------------------------------------------------------------------ Layer 0
;; File patch helpers

(defn- apply-line-replacement
  "Replace the first occurrence of :current with :suggested on the violation's line.
   lines is a vector of strings (0-indexed). line numbers in violations are 1-indexed."
  [lines violation]
  (let [idx       (dec (get violation :line 1))
        current   (get violation :current "")
        suggested (get violation :suggested)]
    (if (and suggested (< idx (count lines)) (str/includes? (get lines idx "") current))
      (update lines idx str/replace-first current suggested)
      lines)))

(defn- prepend-violation?
  "Return true if a violation is a prepend-type remediation."
  [violation]
  (or (= :prepend (get violation :remediation-type))
      (= :std/header-copyright (get violation :rule/id))))

(defn- append-violation?
  "Return true if a violation is an append-type remediation."
  [violation]
  (= :append (get violation :remediation-type)))

(defn- apply-prepend
  "Prepend content from the violation's remediation template."
  [content violation]
  (let [template (get violation :remediation-template)]
    (if template
      (str template "\n" content)
      content)))

(defn- apply-append
  "Append content from the violation's remediation template."
  [content violation]
  (let [template (get violation :remediation-template)]
    (if template
      (if (str/ends-with? content "\n")
        (str content template "\n")
        (str content "\n" template "\n"))
      content)))

(defn patch-file-content
  "Apply all violations for one file to its string content. Pure — no I/O.

   Line-replacement violations are applied bottom-up (descending line number) so
   prior edits do not shift subsequent line indices. Prepend violations
   are applied after all line edits (they add content at position 0). Append
   violations are applied last (they add content at end of file)."
  [content violations]
  (let [prepend-viols (filter prepend-violation? violations)
        append-viols  (filter append-violation? violations)
        line-viols    (remove #(or (prepend-violation? %) (append-violation? %)) violations)
        ;; Preserve trailing newline: split with limit -1 keeps trailing empty segment
        lines         (str/split content #"\n" -1)
        patched-lines (reduce apply-line-replacement lines (sort-by :line > line-viols))
        patched       (str/join "\n" patched-lines)
        ;; Apply prepends then appends
        patched       (if (seq prepend-viols)
                        (apply-prepend patched (first prepend-viols))
                        patched)
        patched       (if (seq append-viols)
                        (reduce apply-append patched append-viols)
                        patched)]
    patched))

(def ^:private semantic-repair-template
  "Template for LLM semantic repair prompts.
   Pack-bundled templates (PR #463) will supersede this when wired."
  (str "Fix the following code violation.\n\n"
       "**Rule:** %s\n"
       "**File:** %s:%s\n"
       "**Current code:** `%s`\n"
       "**Issue:** %s\n"
       "%s\n"
       "\nProvide the corrected code only, no explanation."))

(defn build-semantic-repair-prompt
  "Build a structured prompt for LLM-based semantic repair of a violation.

   This is the dispatch path for :semantic remediation strategy violations.
   The actual LLM call is deferred — this function produces the prompt.

   Arguments:
   - violation — Violation map with :rule/id, :file, :line, :current, :rationale
   - rule      — Full rule map with :rule/knowledge-content

   Returns:
   - {:role :user :content string}"
  [violation rule]
  (let [reference (when-let [kc (get rule :rule/knowledge-content)]
                    (str "\n**Reference:**\n" kc))]
    {:role    :user
     :content (format semantic-repair-template
                      (get rule :rule/title (name (get violation :rule/id)))
                      (get violation :file "")
                      (get violation :line "")
                      (get violation :current "")
                      (get violation :rationale "")
                      (or reference ""))}))

(defn- patch-file!
  "Apply auto-fixable violations to a file on disk. No-op if content is unchanged.
   Returns the absolute file path."
  [repo-path file violations]
  (let [path    (str repo-path "/" file)
        content (slurp path)
        patched (patch-file-content content violations)]
    (when-not (= content patched)
      (spit path patched))
    path))

;------------------------------------------------------------------------------ Layer 1
;; Git shell helpers

(defn- git!
  "Run a git command in repo-path directory. Returns the sh result map.
   Logs non-zero exit codes for debugging (branch -D failures are expected)."
  [repo-path & args]
  (let [result (apply shell/sh "git" (concat args [:dir repo-path]))]
    (when (and (not (zero? (:exit result)))
               (not (str/includes? (str args) "branch")))  ;; branch -D may fail safely
      (println (str "  git " (str/join " " args) " → exit " (:exit result)))
      (when-not (str/blank? (:err result))
        (println (str "  stderr: " (str/trim (:err result))))))
    result))

(defn- branch-slug
  "Convert a rule category and title to a valid git branch segment."
  [category title]
  (-> (str/lower-case title)
      (str/replace #"[^a-z0-9]+" "-")
      (str/replace #"-+$" "")
      (->> (str "fix/compliance-" category "-"))))

(defn- reset-to-origin-main!
  "Detach HEAD at origin/main, giving a clean base for the next rule branch."
  [repo-path]
  (git! repo-path "fetch" "origin" "main" "--quiet")
  (git! repo-path "checkout" "--detach" "origin/main"))

(defn- create-rule-branch!
  "Create and checkout a new branch from origin/main. Deletes if already exists."
  [repo-path branch]
  (git! repo-path "branch" "-D" branch)  ;; safe to fail if absent
  (git! repo-path "checkout" "-b" branch "origin/main"))

;------------------------------------------------------------------------------ Layer 1.5
;; PR documentation (pure)

(defn- today-str
  "Return today's date as YYYY-MM-DD."
  []
  (str (java.time.LocalDate/now)))

(defn- pr-doc-filename
  "Generate the docs/pull-requests/ filename for a compliance PR."
  [category title]
  (let [slug (-> (str/lower-case title)
                 (str/replace #"[^a-z0-9]+" "-")
                 (str/replace #"-+$" ""))]
    (str "docs/pull-requests/" (today-str) "-fix-" category "-" slug ".md")))

(defn- format-violation-line
  "Format a single violation as a markdown table row."
  [v]
  (str "| `" (get v :file "") "` | " (get v :line 0)
       " | `" (get v :current "") "`"
       (when-let [s (get v :suggested)]
         (str " | `" s "`"))
       " |"))

(defn- build-pr-doc
  "Generate the PR documentation markdown for a compliance fix.
   Pure function — returns the markdown string."
  [branch category title patched-files violations-fixed auto-violations]
  (let [file-count (count patched-files)]
    (str "# fix: [" category "] " title " compliance pass\n"
         "\n"
         "**Branch:** `" branch "`\n"
         "**Date:** " (today-str) "\n"
         "\n"
         "---\n"
         "```yaml\n"
         "generated-by: miniforge-compliance-scanner\n"
         "rule-id: " category "\n"
         "rule-title: \"" title "\"\n"
         "fix-type: mechanical\n"
         "violations-fixed: " violations-fixed "\n"
         "files-changed: " file-count "\n"
         "date: " (today-str) "\n"
         "```\n"
         "---\n"
         "\n"
         "## Summary\n"
         "\n"
         "Automated compliance pass applying mechanical fixes for **" title "** (Dewey " category ").\n"
         violations-fixed " violation" (when (> violations-fixed 1) "s")
         " fixed across " file-count " file" (when (> file-count 1) "s") ".\n"
         "\n"
         "## Files Changed\n"
         "\n"
         "| File | Line | Current | Suggested |\n"
         "|------|------|---------|-----------|\n"
         (str/join "\n" (map format-violation-line auto-violations))
         "\n"
         "\n"
         "## Verification\n"
         "\n"
         "- Re-run `bb miniforge run docs/compliance/miniforge-scan.md` to confirm zero violations for this rule\n"
         "- All fixes are mechanical — no semantic changes\n"
         "\n"
         "---\n"
         "*Auto-generated by the Miniforge compliance scanner.*\n")))

(defn- build-pr-body
  "Generate the GitHub PR body with YAML attribution frontmatter."
  [category title violations-fixed file-count pr-doc-path]
  (str "```yaml\n"
       "generated-by: miniforge-compliance-scanner\n"
       "rule-id: " category "\n"
       "rule-title: \"" title "\"\n"
       "fix-type: mechanical\n"
       "violations-fixed: " violations-fixed "\n"
       "files-changed: " file-count "\n"
       "date: " (today-str) "\n"
       "```\n"
       "\n"
       "Applies mechanical fixes for all auto-fixable violations of this rule.\n"
       "Violations requiring semantic review are excluded — see the work spec\n"
       "at `docs/compliance/` for the full list.\n"
       "\n"
       "PR documentation: [`" pr-doc-path "`](" pr-doc-path ")\n"
       "\n"
       "\uD83E\uDD16 Generated with [Miniforge](https://miniforge.ai)"))

(defn- write-pr-doc!
  "Write the PR documentation file. Creates parent directories if needed.
   Returns the relative path written."
  [repo-path relative-path content]
  (let [f (io/file repo-path relative-path)]
    (io/make-parents f)
    (spit f content)
    relative-path))

;------------------------------------------------------------------------------ Layer 2
;; Per-rule fix, commit, and PR

(defn- fix-and-pr-for-rule!
  "Create a branch, apply auto-fixable fixes, write PR doc, commit, push, and open a PR.
   Returns a result map with :branch, :pr-url, :violations-fixed, :files-changed, :pr-doc."
  [repo-path rule-id category title tasks]
  (let [branch   (branch-slug category title)
        pr-title (str "fix: [" category "] " title " compliance pass")]
    (reset-to-origin-main! repo-path)
    (create-rule-branch! repo-path branch)
    (let [auto-tasks      (filter #(some :auto-fixable? (get % :task/violations)) tasks)
          patched-files   (mapv (fn [task]
                                  (let [file  (get task :task/file)
                                        viols (filter :auto-fixable? (get task :task/violations))]
                                    (patch-file! repo-path file viols)
                                    file))
                                auto-tasks)
          auto-violations (->> auto-tasks
                               (mapcat #(filter :auto-fixable? (get % :task/violations)))
                               vec)
          violations-fixed (count auto-violations)
          ;; Write PR doc
          pr-doc-path     (pr-doc-filename category title)
          pr-doc-content  (build-pr-doc branch category title patched-files
                                        violations-fixed auto-violations)
          _               (write-pr-doc! repo-path pr-doc-path pr-doc-content)
          all-files       (conj patched-files pr-doc-path)]
      ;; Stage changed files + PR doc
      (apply git! repo-path "add" all-files)
      ;; Disable GPG/SSH signing — 1Password agent blocks in subprocess contexts
      (git! repo-path "config" "commit.gpgsign" "false")
      ;; Commit — skip pre-commit hook; changes are mechanically verified by the scanner
      (git! repo-path "commit" "--no-verify" "-m"
            (str "fix: [" category "] " title " compliance pass ("
                 (count patched-files) " file" (when (> (count patched-files) 1) "s") ")"))
      ;; Push (force-with-lease in case branch already exists from a prior run)
      (git! repo-path "push" "origin"
            (str branch ":refs/heads/" branch) "--force-with-lease")
      ;; Create PR via gh CLI
      (let [pr-body   (build-pr-body category title violations-fixed
                                     (count patched-files) pr-doc-path)
            pr-result (shell/sh "gh" "pr" "create"
                                "--base"  "main"
                                "--head"  branch
                                "--title" pr-title
                                "--body"  pr-body
                                :dir repo-path)
            pr-url    (str/trim (:out pr-result))]
        {:rule/id          rule-id
         :branch           branch
         :pr-url           pr-url
         :pr-doc           pr-doc-path
         :violations-fixed violations-fixed
         :files-changed    (count patched-files)}))))

;------------------------------------------------------------------------------ Layer 3
;; Top-level entry point

(defn execute!
  "Apply all auto-fixable violations and create one PR per rule.

   Reads DAG tasks from plan, groups by :task/rule-id, and for each rule
   with auto-fixable violations: creates a branch, patches files, commits,
   pushes, and opens a GitHub PR.

   Arguments:
   - plan      - Plan map with :dag-tasks
   - repo-path - string path to the repo/worktree root

   Returns map with :prs (vector), :violations-fixed (int), :files-changed (int)."
  [plan repo-path]
  (let [dag-tasks  (get plan :dag-tasks [])
        auto-tasks (filter #(some :auto-fixable? (get % :task/violations)) dag-tasks)
        by-rule    (group-by :task/rule-id auto-tasks)]
    (if (empty? by-rule)
      {:prs [] :violations-fixed 0 :files-changed 0}
      (let [prs (mapv (fn [[rule-id tasks]]
                        (let [first-v (first (get (first tasks) :task/violations []))]
                          (fix-and-pr-for-rule! repo-path rule-id
                                                (get first-v :rule/category "0")
                                                (get first-v :rule/title (name rule-id))
                                                tasks)))
                      by-rule)]
        {:prs              prs
         :violations-fixed (transduce (map :violations-fixed) + 0 prs)
         :files-changed    (transduce (map :files-changed)    + 0 prs)}))))

;------------------------------------------------------------------------------ Rich Comment
(comment
  (patch-file-content
   "(get m :timeout 5000)"
   [{:rule/id :std/clojure :line 1
     :current "(get m :timeout 5000)" :suggested "(get m :timeout 5000)"
     :auto-fixable? true}])

  (patch-file-content
   "# My Doc\n\nSome content.\n"
   [{:rule/id :std/header-copyright :line 1
     :current "(missing copyright header)" :suggested nil
     :auto-fixable? true}])

  :leave-this-here)
