(ns wanijo.instance.db
  (:require [clojure.spec.alpha :as spec]
            [wanijo.infrastructure.neo4j :as neo4j]
            [wanijo.instance.domain :as domain-instance]
            [wanijo.tag.db :as db-tag]))

(neo4j/defquery findy-by-schema
  "MATCH (i:instance)-[:of]->(s:schema)
   WHERE s.uuid = {uuid}
   OPTIONAL MATCH
         (i)-[:tagged_with]->(t:tag)
   OPTIONAL MATCH
         (i)<-[:of]-(p:property),
         (p)-[:of]->(a:attribute)
   RETURN i, t, p, a")
(defn flatten-grouped-rows [instance grouped-rows]
  {:post [(spec/assert ::domain-instance/instance-with-tags-and-props %)]}
  (assoc instance
         :tags (->> grouped-rows
                    (map :t)
                    (filter some?)
                    distinct)
         :properties (->> grouped-rows
                          (filter #(some? (:p %)))
                          (map #(assoc (:p %)
                                       :attribute (:a %)))
                          distinct)))
(defn find-by-schema! [schema-uuid]
  {:post [(spec/assert ::domain-instance/instances-with-tags-and-props %)]}
  (->> (neo4j/exec-query! findy-by-schema
                          {:uuid schema-uuid})
       (group-by :i)
       (map #(flatten-grouped-rows (key %) (val %)))
       (sort-by :name)))

(neo4j/defquery create-instance
  "MATCH (s:schema {uuid:{schema_uuid}}),
         (u:user {uuid:{user_uuid}})
   CREATE (i:instance {uuid:{uuid}})-[:of]->(s),
          (i)-[:created_by]->(u)
      SET i.name = {name},
          i.created_at = {created_at},
          i.updated_at = {created_at}")
(neo4j/defquery create-property
  "MATCH (i:instance {uuid:{uuid}}),
         (a:attribute {uuid:{attr_uuid}})
   CREATE (p:property {uuid:{prop_uuid}})-[:of]->(i),
          (p)-[:of]->(a)
     SET p.value = {value},
         p.created_at = {created_at},
         p.updated_at = {updated_at}")
(defn create! [user-uuid
               schema-uuid
               {:keys [name properties]}]
  (let [instance-uuid (neo4j/uuid)
        now (neo4j/now-str)
        instance-tuple [create-instance
                        {:schema_uuid schema-uuid
                         :name name
                         :uuid instance-uuid
                         :created_at now
                         :user_uuid user-uuid}]
        prop-tuples (for [{:keys [attribute value]} properties]
                      [create-property
                       {:uuid instance-uuid
                        :attr_uuid (:uuid attribute)
                        :prop_uuid (neo4j/uuid)
                        :value value
                        :created_at now
                        :updated_at now}])]
    (apply neo4j/exec-queries!
           (concat [instance-tuple]
                   prop-tuples))))

(neo4j/defquery find-by-uuid
  "MATCH (i:instance {uuid:{uuid}})-[:of]->(s:schema)
   RETURN i, s")
(defn find-by-uuid! [uuid]
  {:post [(spec/assert ::domain-instance/instance-with-schema %)]}
  (->> (neo4j/exec-query! find-by-uuid
                          {:uuid uuid})
       (map #(assoc (:i %)
                    :schema
                    (:s %)))
       first))

(neo4j/defquery find-properties
  "MATCH (i:instance {uuid:{uuid}}),
         (p:property)-[:of]->(i),
         (p)-[:of]->(a:attribute)
   RETURN p, a
    ORDER BY a.name")
(defn find-properties! [uuid]
  (map #(assoc (:p %)
               :attribute
               (:a %))
       (neo4j/exec-query! find-properties
                          {:uuid uuid})))

(neo4j/defquery edit-instance
  "MATCH (i:instance {uuid:{uuid}})
     SET i.name = {name},
         i.updated_at = {updated_at}")
(neo4j/defquery edit-property
  "MATCH (i:instance {uuid:{instance_uuid}}),
         (a:attribute {uuid:{attribute_uuid}})
   MERGE (p:property {uuid:{uuid}})-[:of]->(i)
   MERGE (p)-[:of]->(a)
   ON CREATE SET p.created_at = {now},
                 p.updated_at = {now},
                 p.value = {value}
   ON MATCH SET p.updated_at = {now},
                p.value = {value}")
(defn instance->prop-tuples [instance]
  {:post [(spec/assert ::domain-instance/prop-tuple %)]}
  (map (fn [prop]
         (let [prop-uuid (:uuid prop)
               uuid (if (empty? prop-uuid)
                      (neo4j/uuid)
                      prop-uuid)]
           [edit-property
            {:uuid uuid
             :now (neo4j/now-str)
             :value (:value prop)
             :instance_uuid (:uuid instance)
             :attribute_uuid (-> prop :attribute :uuid)}]))
       (:properties instance)))
(defn edit! [instance]
  (let [prop-tuples (instance->prop-tuples instance)]
    (apply neo4j/exec-queries!
           (concat [[edit-instance
                     {:uuid (:uuid instance)
                      :name (:name instance)
                      :updated_at (neo4j/now-str)}]]
                   prop-tuples))))

(neo4j/defquery delete
  "MATCH (i:instance {uuid:{uuid}}),
         (i)-[ic:of]->(s:schema),
         (i)-[cb:created_by]->(:user)
   OPTIONAL MATCH
         (p:property)-[pc:of]->(i),
         (p)-[pac:of]->(a:attribute)
   OPTIONAL MATCH
         (i)-[tw:tagged_with]->()
   OPTIONAL MATCH
         (i)-[l:link]-()
   DELETE pac, pc, cb, ic, p,
          l, tw, i")
(defn delete! [uuid]
  (neo4j/exec-query! delete {:uuid uuid}))

(defn group-by-link-and-assoc-tags [uuid query direction]
  (->> (neo4j/exec-query! query {:uuid uuid})
       (group-by #(hash-map :link (:link %)
                            :schema (:schema %)
                            direction (direction %)))
       (map (fn [[link tags]]
              (let [tags (map :tag tags)]
                (assoc link
                       :tags (filter some? tags)))))
       (sort-by (juxt #(-> % :schema :name)
                      #(-> % direction :name)
                      #(-> % direction :created_at)))))

(neo4j/defquery outgoing-links
  "MATCH (inst:instance {uuid:{uuid}}),
         (inst)-[link:link]->(target:instance),
         (target)-[:of]->(schema:schema)
   OPTIONAL MATCH
         (target)-[:tagged_with]->(tag:tag)
   RETURN link, target, schema, tag")
(defn outgoing-links! [uuid]
  {:post [(spec/assert ::domain-instance/links-out %)]}
  (group-by-link-and-assoc-tags uuid outgoing-links :target))

(neo4j/defquery incoming-links
  "MATCH (inst:instance {uuid:{uuid}}),
         (inst)<-[link:link]-(source:instance),
         (source)-[:of]->(schema:schema)
   OPTIONAL MATCH
         (source)-[:tagged_with]->(tag:tag)
   RETURN link, source, schema, tag
   ORDER BY schema.name, source.name, source.created_at")
(defn incoming-links! [uuid]
  {:post [(spec/assert ::domain-instance/links-in %)]}
  (group-by-link-and-assoc-tags uuid incoming-links :source))

(defn full-instance-by-uuid! [uuid]
  {:post [(spec/assert ::domain-instance/full-instance %)]}
  (assoc (find-by-uuid! uuid)
         :properties
         (find-properties! uuid)
         :links-out
         (outgoing-links! uuid)
         :links-in
         (incoming-links! uuid)
         :tags
         (db-tag/tags-by-instance! uuid)))

(neo4j/defquery is-starred
  "MATCH (u:user {uuid:{user_uuid}}),
         (i:instance {uuid:{uuid}})
   RETURN EXISTS((i)-[:starred_by]->(u)) AS starred")
(defn is-starred! [uuid user-uuid]
  (-> (neo4j/exec-query! is-starred
                         {:user_uuid user-uuid
                          :uuid uuid})
      first
      :starred))

(neo4j/defquery mark-starred
  "MATCH (i:instance {uuid:{uuid}}),
         (u:user {uuid:{user_uuid}})
   MERGE (i)-[s:starred_by]->(u)
   ON CREATE
   SET s.created_at = {now}")
(defn mark-starred! [uuid user-uuid]
  (neo4j/exec-query! mark-starred
                     {:uuid uuid
                      :user_uuid user-uuid
                      :now (neo4j/now-str)}))

(neo4j/defquery remove-starred
  "MATCH (:instance {uuid:{uuid}})
         -[s:starred_by]->
         (:user {uuid:{user_uuid}})
   DELETE s")
(defn remove-starred! [uuid user-uuid]
  (neo4j/exec-query! remove-starred
                     {:uuid uuid
                      :user_uuid user-uuid}))

(neo4j/defquery starred-by-user
  "MATCH (u:user {uuid:{user_uuid}})
         <-[s:starred_by]-
         (i:instance)
   RETURN i, s
   ORDER BY s.created_at DESC")
(defn starred-by-user! [user-uuid]
  (map #(assoc (:i %)
               :starred_at (-> % :s :created_at))
       (neo4j/exec-query! starred-by-user
                          {:user_uuid user-uuid})))
