 (ns mvxcvi.vault.tool
-  (:require [clojure.tools.cli :refer [cli]])
+  (:require [clojure.string :as string]
+            [clojure.tools.cli :refer [cli]])
   (:gen-class :main true))
 
 ;; Vault commands need to know:

;; $ vault object ...
;; $ vault group ...

;; So what's the algorithm?
;; - scan arguments for first occurrance of subcommand key
;;   NOTE: this means that an argument to an option CANNOT be any subcommand name!
;;   e.g. $ vault --some-option blob --another-option foo group show ...
;;        $ vault [--some-option] [blob --another-option foo ...]
;;
;; - run cli with the specs for this node on the pre-command args
;; - if the subcommand is 'help', change it to :help true in the opts map and recur
;; - if a subcommand was chosen, recurse with the subcommand node, the subcommand args, and the merged option map
;; - if no subcommand was chosen and :help is set, print the help message for this node
;; - if no subcommand was chosen and the node has an :action, execute the action with the opts and remaining args
;; - otherwise, error.




; command node:
#_
{:usage "vault [global opts] <command> [command args]"
 :desc "Command-line tool for the vault data store."
 :specs [["--blob-store" "Path to blob-store configuration." ...]]
 :init (fn [opts] ...)

 :action (fn [opts args] ...)}


(def ^:private ^:dynamic *command-branch*
  "Vector containing the command names on the current branch."
  [])


(defmacro ^:private command
  "Simple macro for building readable command trees."
  [usage desc & more]
  (let [[cname usage] (string/split usage #" " 2)
        [specs more] (split-with vector? more)
        command {:name cname
                 :usage `(string/join " " (concat *command-branch* [~usage]))
                 :desc desc
                 :specs (vec specs)}
        elements (group-by first (filter list? more))]
    (when (> (count (elements 'init)) 1)
      (throw (IllegalArgumentException.
               (str "Multiple `init` elements in command definition: " (elements 'init)))))
    (when (> (count (elements 'action)) 1)
      (throw (IllegalArgumentException.
               (str "Multiple `action` elements in command definition: " (elements 'action)))))
    (when (and (elements 'command) (elements 'action))
      (throw (IllegalArgumentException.
               (str "Command definition contains both `command` and `action` elements: " more))))
    (let [assoc-fn (fn [cmd [k & body]]
                     (if k
                       (if (symbol? (first body))
                         (assoc cmd (keyword k) (first body))
                         (assoc cmd (keyword k) `(fn ~@body)))
                       cmd))
          command (reduce assoc-fn command
                          [(first (elements 'init))
                           (first (elements 'action))])]
      (if-let [subcommands (elements 'command)]
        `(binding [*command-branch* (conj *command-branch* ~(:name command))]
           (->>
             ~(vec subcommands)
             (map (juxt :name identity))
             (into {})
             (assoc ~command :commands)))
        `(binding [*command-branch* (conj *command-branch* ~(:name command))]
           ~command)))))


(defn- handle-command
  "Handles a sequence of arguments following a command tree structure."
  [command opts args]
  (let [subcommand-names (into #{} (cons "help" (keys (:commands command))))

