(ns examples.real-estate
  "Read along: http://curtclifton.net/papers/MoseleyMarks06a.pdf"
  (:require [com.wotbrew.relic :as rel]
            [clojure.string :as str]))

;; *functional* relational programming
;; just use clojure functions & predicates on rows

(defn- price-band [price]
  (condp >= price
    300000M :low
    650000M :med
    1000000M :high
    :premium))

(defn- area-code [address]
  ;; we are just pretending
  (last (str/split address #" ")))

(defn- speed-band [date1 date2]
  (let [day-ms (* 1000 60 60 24)]
    (condp >= (/ (- (inst-ms date1) (inst-ms date2)) day-ms)
      10 :very-fast
      20 :fast
      30 :medium
      60 :slow
      :very-slow)))

;; derived relvars

(def RoomInfo
  [[:from ::Room]
   [:extend [:room-size [* :width :breadth]]]])

(def Acceptance
  [[:from ::Decision]
   [:where :accepted]
   [:without :accepted]])

(def Rejection
  [[:from ::Decision]
   [:where [not :accepted]]
   [:without :accepted]])

(def PropertyInfo
  [[:from ::Property]
   [:extend
    [:price-band [price-band :price]]
    [:area-code [area-code :address]]

    [[:number-of-rooms]
     [rel/sel1
      [[:from RoomInfo]
       [:agg [:address] [:number-of-rooms count]]]
      {:address :address}]]

    [[:square-feet]
     [rel/sel1
      [[:from RoomInfo]
       [:agg [:address] [:square-feet [rel/sum :room-size]]]]
      {:address :address}]]]])

(def CurrentOffer
  [[:from ::Offer]
   [:agg [:address :bidder-name :bidder-address]
    [:latest-date [max :offer-date]]]
   [:join ::Offer {:address :address
                   :bidder-name :bidder-name
                   :bidder-address :bidder-address
                   :latest-date :offer-date}]])

(def RawSales
  [[:from Acceptance]
   [:join CurrentOffer {:address :address
                        :bidder-name :bidder-name
                        :bidder-address :bidder-address}]
   [:join ::Property {:address :address}]
   [:without :offer-date :bidder-name :bidder-address]])

(def SoldProperty
  [[:from RawSales]
   [:select :address]])

(def UnsoldProperty
  [[:from ::Property]
   [:select :address]
   [:difference SoldProperty]])

(def SalesInfo
  [[:from RawSales]
   [:select
    :address
    :agent
    [:area-code [area-code :address]]
    [:sale-speed [speed-band :date-registered :decision-date]]
    [:price-band [price-band :offer-price]]]])

(def SalesCommissions
  [[:from SalesInfo]
   [:join ::Commission {:agent :agent
                        :sale-speed :sale-speed
                        :price-band :price-band}]
   [:select :address :agent :commission]])

;; external

(def OpenOffers
  [[:from CurrentOffer]
   [:join [[:from CurrentOffer]
           [:without :offer-price :latest-date]
           [:difference [[:from ::Decision]
                         [:without :accepted :decision-date]]]]
    {:bidder-name :bidder-name
     :bidder-address :bidder-address
     :address :address}]])

(def PropertyForWebsite
  [[:from UnsoldProperty]
   [:join PropertyInfo {:address :address}]
   [:select :address :price :photo :number-of-rooms :square-feet]])

(def CommissionDue
  [[:from SalesCommissions]
   [:agg [:agent]
    [:total-commission [rel/sum :commission]]]
   [:select :agent :total-commission]])

;; constraints

(def PropertyKey
  [[:from ::Property]
   [:unique :address]])

(def OfferKey
  [[:from ::Offer]
   [:unique :address :offer-date :bidder-name :bidder-address]])

(def DecisionKey
  [[:from ::Decision]
   [:unique :address :offer-date :bidder-name :bidder-address]])

(def RoomKey
  [[:from ::Room]
   [:unique :address :room-name]])

(def FloorKey
  [[:from ::Floor]
   [:unique :address :room-name]])

(def CommissionKey
  [[:from ::Commission]
   [:unique :price-band :area-code :sale-speed]])

(def OfferToProperty
  [[:from ::Offer]
   [:fk ::Property {:address :address}]])

(def DecisionToOffer
  [[:from ::Decision]
   [:fk ::Offer {:address :address
                 :offer-date :offer-date
                 :bidder-name :bidder-name
                 :bidder-address :bidder-address}]])

(def RoomToProperty
  [[:from ::Room]
   [:fk ::Property {:address :address}]])

(def FloorToProperty
  [[:from ::Floor]
   [:fk ::Property {:address :address}]])

(def PropertyHasToHaveAtLeastOneRoom
  [[:from PropertyInfo]
   [:check [:? <= 1 :number-of-rooms]]])

(def CannotBidOnOwnProperty
  [[:from ::Offer]
   [:check [not= :address :bidder-address]]])

(def CannotSubmitOfferOnceSaleAgreed
  [[:from ::Offer]
   [:join [[:from Acceptance] [:select :address :decision-date]] {:address :address}]
   [:check {:pred [:? <= :offer-date :decision-date]
            :error "Offer cannot be submitted after acceptance."}]])

(def NoMoreThan50AdvertisedPremiumProperties
  [[:from PropertyForWebsite]
   [:agg [] [:premium-count [count [= [:_ :premium] [price-band :price]]]]]
   [:check {:pred [:<= :premium-count 50]
            :error "At most 50 properties can be advertised on the website."}]])

(def NoSingleBidderCanSubmitMoreThan10OffersOnAProperty
  [[:from ::Offer]
   [:agg [:address :bidder-address :bidder-name] [:number-of-offers count]]
   [:check {:pred [<= :number-of-offers 10]
            :error "No single bidder can submit more than 10 offers on a property."}]])

(defn constrain [db]
  (rel/mat
    db
    PropertyKey
    OfferKey
    DecisionKey
    RoomKey
    FloorKey
    CommissionKey

    OfferToProperty
    DecisionToOffer
    RoomToProperty
    FloorToProperty
    PropertyHasToHaveAtLeastOneRoom

    CannotBidOnOwnProperty
    CannotSubmitOfferOnceSaleAgreed
    NoMoreThan50AdvertisedPremiumProperties
    NoSingleBidderCanSubmitMoreThan10OffersOnAProperty))

(defn new-database []
  (-> {}
      (constrain)
      (rel/mat PropertyInfo)))

(def data
  (let [address1 "abc def 55"
        alice-address "wonderland 42"]
    {::Property [{:address address1
                  :price 344000M
                  :photo "foo.jpg"
                  :agent "bob"
                  :date-registered #inst "2021-10-26"}
                 {:address alice-address
                  :price 1690000M
                  :photo "foo.jpg"
                  :agent "bob"
                  :date-registered #inst "2021-10-27"}]
     ::Offer [{:address address1
               :offer-date #inst "2021-10-27"
               :offer-price 343000M
               :bidder-name "alice"
               :bidder-address alice-address}
              {:address address1
               :offer-date #inst "2021-10-26"
               :offer-price 150000M
               :bidder-name "alice"
               :bidder-address alice-address}]
     ::Decision [{:address address1
                  :offer-date #inst "2021-10-27"
                  :bidder-name "alice"
                  :bidder-address alice-address
                  :decision-date #inst "2021-10-28"
                  :accepted true}]
     ::Room [{:address address1
              :room-name "Room 1"
              :width 10.0M
              :breadth 10.0M
              :type :living-room}
             {:address alice-address
              :room-name "Room 1"
              :width 12.0M
              :breadth 14.5M
              :type :living-room}
             {:address alice-address
              :room-name "Kitchen"
              :width 13.0M
              :breadth 8.0M
              :type :kitchen}]
     ::Floor [{:address address1
               :room-name "Room 1"
               :floor 0}]
     ::Commission [{:agent "bob"
                    :price-band :med
                    :area-code "55"
                    :sale-speed :very-fast
                    :commission 2000.0M}]}))