(ns metabase.channel.impl.email
  (:require
   [buddy.core.codecs :as codecs]
   [hiccup.core :refer [html]]
   [medley.core :as m]
   [metabase.channel.core :as channel]
   [metabase.channel.params :as channel.params]
   [metabase.channel.render.core :as channel.render]
   [metabase.channel.template.handlebars :as handlebars]
   [metabase.email :as email]
   [metabase.email.messages :as messages]
   [metabase.email.result-attachment :as email.result-attachment]
   [metabase.models.channel :as models.channel]
   [metabase.models.notification :as models.notification]
   [metabase.models.params.shared :as shared.params]
   [metabase.public-settings :as public-settings]
   [metabase.util :as u]
   [metabase.util.encryption :as encryption]
   [metabase.util.i18n :refer [trs]]
   [metabase.util.json :as json]
   [metabase.util.log :as log]
   [metabase.util.malli :as mu]
   [metabase.util.malli.schema :as ms]
   [metabase.util.markdown :as markdown]
   [metabase.util.ui-logic :as ui-logic]
   [metabase.util.urls :as urls]
   [ring.util.codec :as codec]))
(def ^:private EmailMessage
  [:map
   [:subject                         :string]
   [:recipients                      [:sequential ms/Email]]
   [:message-type                    [:enum :attachments :html :text]]
   [:message                         :any]
   [:recipient-type {:optional true} [:maybe (ms/enum-keywords-and-strings :cc :bcc)]]])
(mu/defmethod channel/send! :channel/email
  [_channel {:keys [subject recipients message-type message recipient-type]} :- EmailMessage]
  (email/send-message-or-throw! {:subject      subject
                                 :recipients   recipients
                                 :message-type message-type
                                 :message      message
                                 :bcc?         (if recipient-type
                                                 (= :bcc recipient-type)
                                                 (email/bcc-enabled?))}))

------------------------------------------------------------------------------------------------;; Render Utils ;; ------------------------------------------------------------------------------------------------;;

Generates hash to allow for non-users to unsubscribe from pulses/subscriptions.

(defn generate-dashboard-sub-unsubscribe-hash
  [pulse-id email]
  (codecs/bytes->hex
   (encryption/validate-and-hash-secret-key
    (json/encode {:salt     (public-settings/site-uuid-for-unsubscribing-url)
                  :email    email
                  :pulse-id pulse-id}))))
(defn- unsubscribe-url-for-non-user
  [dashboard-subscription-id non-user-email]
  (str (urls/unsubscribe-url)
       "?"
       (codec/form-encode {:hash     (generate-dashboard-sub-unsubscribe-hash dashboard-subscription-id non-user-email)
                           :email    non-user-email
                           :pulse-id dashboard-subscription-id})))
(defn- render-part
  [timezone part options]
  (case (:type part)
    :card
    (channel.render/render-pulse-section timezone part options)
    :text
    {:content (markdown/process-markdown (:text part) :html)}
    :tab-title
    {:content (markdown/process-markdown (format "# %s\n---" (:text part)) :html)}))
(defn- render-body
  [{:keys [details] :as _template} payload]
  (case (keyword (:type details))
    :email/handlebars-resource
    (handlebars/render (:path details) payload)
    :email/handlebars-text
    (handlebars/render-string (:body details) payload)
    (do
      (log/warnf "Unknown email template type: %s" (:type details))
      nil)))
(defn- render-message-body
  [template message-context attachments]
  (vec (concat [{:type "text/html; charset=utf-8" :content (render-body template message-context)}] attachments)))
(defn- make-message-attachment [[content-id url]]
  {:type         :inline
   :content-id   content-id
   :content-type "image/png"
   :content      url})
(defn- assoc-attachment-booleans [part-configs parts]
  (for [{{result-card-id :id} :card :as result} parts
        ;; TODO: check if does this match by dashboard_card_id or card_id?
        :let [noti-dashcard (m/find-first #(= (:card_id %) result-card-id) part-configs)]]
    (if result-card-id
      (update result :card merge (select-keys noti-dashcard [:include_csv :include_xls :format_rows :pivot_results]))
      result)))
(defn- email-attachment
  [rendered-cards parts]
  (filter some?
          (concat (map make-message-attachment (apply merge (map :attachments (u/one-or-many rendered-cards))))
                  (mapcat email.result-attachment/result-attachment parts))))

Bundle an icon.

The available icons are defined in [[render.js.svg/icon-paths]].

(defn- icon-bundle
  [icon-name]
  (let [color     (channel.render/primary-color)
        png-bytes (channel.render/icon icon-name color)]
    (-> (channel.render/make-image-bundle :attachment png-bytes)
        (channel.render/image-bundle->attachment))))
(defn- construct-email
  ([subject recipients message]
   (construct-email subject recipients message nil))
  ([subject recipients message recipient-type]
   {:subject        subject
    :recipients     recipients
    :message-type   :attachments
    :message        message
    :recipient-type recipient-type}))
(defn- recipients->emails
  [recipients]
  (update-vals
   {:user-emails     (mapv (comp :email :user) (filter #(= :user (:kind %)) recipients))
    :non-user-emails (mapv :email (filter #(= :external-email (:kind %)) recipients))}
   #(filter u/email? %)))
(defn- construct-emails
  [template message-context-fn attachments recipients]
  (let [{:keys [user-emails
                non-user-emails]} (recipients->emails recipients)
        email-to-users            (when (seq user-emails)
                                    (let [message-ctx (message-context-fn nil)]
                                      (construct-email
                                       (channel.params/substitute-params (-> template :details :subject) message-ctx)
                                       user-emails
                                       (render-message-body template (message-context-fn nil) attachments))))
        email-to-nonusers         (for [non-user-email non-user-emails]
                                    (let [message-ctx (message-context-fn non-user-email)]
                                      (construct-email
                                       (channel.params/substitute-params (-> template :details :subject) message-ctx)
                                       [non-user-email]
                                       (render-message-body template (message-context-fn non-user-email) attachments))))]
    (filter some? (conj email-to-nonusers email-to-users))))

------------------------------------------------------------------------------------------------;; Alerts ;; ------------------------------------------------------------------------------------------------;;

(mu/defmethod channel/render-notification [:channel/email :notification/card] :- [:sequential EmailMessage]
  [_channel-type {:keys [payload] :as notification-payload} template recipients]
  (let [{:keys [card_part
                alert
                card]}     payload
        timezone           (channel.render/defaulted-timezone card)
        rendered-card      (render-part timezone card_part {:channel.render/include-title? true})
        icon-attachment    (apply make-message-attachment (icon-bundle :bell))
        attachments        (concat [icon-attachment]
                                   (email-attachment rendered-card
                                                     (assoc-attachment-booleans [alert] [card_part])))
        goal               (ui-logic/find-goal-value payload)
        message-context-fn (fn [non-user-email]
                             (assoc notification-payload
                                    :computed {:subject         (case (messages/pulse->alert-condition-kwd alert)
                                                                  :meets (trs "Alert: {0} has reached its goal" (:name card))
                                                                  :below (trs "Alert: {0} has gone below its goal" (:name card))
                                                                  :rows  (trs "Alert: {0} has results" (:name card)))
                                               :icon_cid        (:content-id icon-attachment)
                                               :goal_value      goal
                                               :alert_content   (html (:content rendered-card))
                                               :alert_schedule  (messages/alert-schedule-text (:schedule alert))
                                               :management_text (if (nil? non-user-email)
                                                                  "Manage your subscriptions"
                                                                  "Unsubscribe")
                                               :management_url  (if (nil? non-user-email)
                                                                  (urls/notification-management-url)
                                                                  (unsubscribe-url-for-non-user (:id alert) non-user-email))}))]
    (construct-emails template message-context-fn attachments recipients)))

------------------------------------------------------------------------------------------------;; Dashboard Subscriptions ;; ------------------------------------------------------------------------------------------------;;

(defn- render-filters
  [parameters]
  (let [cells (map
               (fn [filter]
                 [:td {:class "filter-cell"
                       :style (channel.render/style {:width "50%"
                                                     :padding "0px"
                                                     :vertical-align "baseline"})}
                  [:table {:cellpadding "0"
                           :cellspacing "0"
                           :width "100%"
                           :height "100%"}
                   [:tr
                    [:td
                     {:style (channel.render/style {:color channel.render/color-text-medium
                                                    :min-width "100px"
                                                    :width "50%"
                                                    :padding "4px 4px 4px 0"
                                                    :vertical-align "baseline"})}
                     (:name filter)]
                    [:td
                     {:style (channel.render/style {:color channel.render/color-text-dark
                                                    :min-width "100px"
                                                    :width "50%"
                                                    :padding "4px 16px 4px 8px"
                                                    :vertical-align "baseline"})}
                     (shared.params/value-string filter (public-settings/site-locale))]]]])
               parameters)
        rows  (partition-all 2 cells)]
    (html
     [:table {:style (channel.render/style {:table-layout    :fixed
                                            :border-collapse :collapse
                                            :cellpadding     "0"
                                            :cellspacing     "0"
                                            :width           "100%"
                                            :font-size       "12px"
                                            :font-weight     700
                                            :margin-top      "8px"})}
      (for [row rows]
        [:tr {} row])])))
(mu/defmethod channel/render-notification [:channel/email :notification/dashboard] :- [:sequential EmailMessage]
  [_channel-type {:keys [payload] :as notification-payload} template recipients]
  (let [{:keys [dashboard_parts
                dashboard_subscription
                parameters
                dashboard]} payload
        timezone            (some->> dashboard_parts (some :card) channel.render/defaulted-timezone)
        rendered-cards      (mapv #(render-part timezone % {:channel.render/include-title? true}) dashboard_parts)
        icon-attachment     (apply make-message-attachment (icon-bundle :dashboard))
        attachments         (concat
                             [icon-attachment]
                             (email-attachment rendered-cards (assoc-attachment-booleans
                                                               (:dashboard_subscription_dashcards dashboard_subscription)
                                                               dashboard_parts)))
        message-context-fn  (fn [non-user-email]
                              (-> notification-payload
                                  (assoc :computed {:dashboard_content  (html (vec (cons :div (map :content rendered-cards))))
                                                    :icon_cid           (:content-id icon-attachment)
                                                    :dashboard_has_tabs (some-> dashboard :tabs seq)
                                                    :management_text    (if (nil? non-user-email)
                                                                          "Manage your subscriptions"
                                                                          "Unsubscribe")
                                                    :management_url     (if (nil? non-user-email)
                                                                          (urls/notification-management-url)
                                                                          (unsubscribe-url-for-non-user (:id dashboard_subscription) non-user-email))
                                                    :filters           (when parameters
                                                                         (render-filters parameters))})
                                  (m/update-existing-in [:payload :dashboard :description] #(markdown/process-markdown % :html))))]
    (construct-emails template message-context-fn attachments recipients)))

------------------------------------------------------------------------------------------------;; System Events ;; ------------------------------------------------------------------------------------------------;;

(defn- notification-recipients->emails
  [recipients notification-payload]
  (into [] cat (for [recipient recipients
                     :let [details (:details recipient)
                           emails (case (:type recipient)
                                    :notification-recipient/user
                                    [(-> recipient :user :email)]
                                    :notification-recipient/group
                                    (->> recipient :permissions_group :members (map :email))
                                    :notification-recipient/external-email
                                    [(:email details)]
                                    :notification-recipient/template
                                    [(not-empty (channel.params/substitute-params (:pattern details) notification-payload :ignore-missing? (:is_optional details)))]
                                    nil)]
                     :let  [emails (filter some? emails)]
                     :when (seq emails)]
                 emails)))
(mu/defmethod channel/render-notification
  [:channel/email :notification/system-event]
  [_channel-type
   notification-payload #_:- #_notification/NotificationPayload
   template             :- models.channel/ChannelTemplate
   recipients           :- [:sequential models.notification/NotificationRecipient]]
  (assert (some? template) "Template is required for system event notifications")
  [(construct-email (channel.params/substitute-params (-> template :details :subject) notification-payload)
                    (notification-recipients->emails recipients notification-payload)
                    [{:type    "text/html; charset=utf-8"
                      :content (render-body template notification-payload)}]
                    (-> template :details :recipient-type keyword))])