# frozen_string_literal: true

class PushNotificationPusher
  TOKEN_VALID_FOR_SECONDS = 5 * 60
  CONNECTION_TIMEOUT_SECONDS = 5

  def self.push(user, payload)
    message = nil
    I18n.with_locale(user.effective_locale) do
      notification_icon_name = Notification.types[payload[:notification_type]]
      if !File.exist?(
           File.expand_path(
             "../../app/assets/images/push-notifications/#{notification_icon_name}.png",
             __dir__,
           ),
         )
        notification_icon_name = "discourse"
      end
      notification_icon =
        ActionController::Base.helpers.image_url("push-notifications/#{notification_icon_name}.png")

      message = {
        title: payload[:translated_title] || title(payload),
        body: payload[:excerpt],
        badge: get_badge,
        icon: notification_icon,
        tag: payload[:tag] || "#{Discourse.current_hostname}-#{payload[:topic_id]}",
        base_url: Discourse.base_url,
        url: payload[:post_url],
      }

      subscriptions(user).each { |subscription| send_notification(user, subscription, message) }
    end

    message
  end

  def self.title(payload)
    translation_key =
      case payload[:notification_type]
      when Notification.types[:watching_category_or_tag]
        # For watching_category_or_tag, the notification could be for either a new post or new topic.
        # Instead of duplicating translations, we can rely on 'watching_first_post' for new topics,
        # and 'posted' for new posts.
        type = payload[:post_number] == 1 ? "watching_first_post" : "posted"
        "discourse_push_notifications.popup.#{type}"
      else
        "discourse_push_notifications.popup.#{Notification.types[payload[:notification_type]]}"
      end

    # Payload modifier used to adjust arguments to the translation
    payload =
      DiscoursePluginRegistry.apply_modifier(:push_notification_pusher_title_payload, payload)

    I18n.t(
      translation_key,
      site_title: SiteSetting.title,
      topic: payload[:topic_title],
      username: payload[:username],
      group_name: payload[:group_name],
    )
  end

  def self.subscriptions(user)
    user.push_subscriptions
  end

  def self.clear_subscriptions(user)
    user.push_subscriptions.clear
  end

  def self.subscribe(user, push_params, send_confirmation)
    data = push_params.to_json
    subscriptions = PushSubscription.where(user: user, data: data)
    subscriptions_count = subscriptions.count

    new_subscription =
      if subscriptions_count > 1
        subscriptions.destroy_all
        PushSubscription.create!(user: user, data: data)
      elsif subscriptions_count == 0
        PushSubscription.create!(user: user, data: data)
      end

    if send_confirmation == "true"
      message = {
        title:
          I18n.t("discourse_push_notifications.popup.confirm_title", site_title: SiteSetting.title),
        body: I18n.t("discourse_push_notifications.popup.confirm_body"),
        icon: ActionController::Base.helpers.image_url("push-notifications/check.png"),
        badge: get_badge,
        tag: "#{Discourse.current_hostname}-subscription",
      }

      send_notification(user, new_subscription, message)
    end
  end

  def self.unsubscribe(user, subscription)
    PushSubscription.find_by(user: user, data: subscription.to_json)&.destroy!
  end

  def self.get_badge
    if (url = SiteSetting.site_push_notifications_icon_url).present?
      url
    else
      ActionController::Base.helpers.image_url("push-notifications/discourse.png")
    end
  end

  MAX_ERRORS = 3
  MIN_ERROR_DURATION = 86_400 # 1 day

  def self.handle_generic_error(subscription, error, user, endpoint, message)
    subscription.error_count += 1
    subscription.first_error_at ||= Time.zone.now

    delta = Time.zone.now - subscription.first_error_at
    if subscription.error_count >= MAX_ERRORS && delta > MIN_ERROR_DURATION
      subscription.destroy!
    else
      subscription.save!
    end

    Discourse.warn_exception(
      error,
      message: "Failed to send push notification",
      env: {
        user_id: user.id,
        endpoint: endpoint,
        message: message.to_json,
      },
    )
  end

  def self.send_notification(user, subscription, message)
    parsed_data = subscription.parsed_data

    endpoint = parsed_data["endpoint"]
    p256dh = parsed_data.dig("keys", "p256dh")
    auth = parsed_data.dig("keys", "auth")

    if (endpoint.blank? || p256dh.blank? || auth.blank?)
      subscription.destroy!
      return
    end

    begin
      WebPush.payload_send(
        endpoint: endpoint,
        message: message.to_json,
        p256dh: p256dh,
        auth: auth,
        vapid: {
          subject: Discourse.base_url,
          public_key: SiteSetting.vapid_public_key,
          private_key: SiteSetting.vapid_private_key,
          expiration: TOKEN_VALID_FOR_SECONDS,
        },
        open_timeout: CONNECTION_TIMEOUT_SECONDS,
        read_timeout: CONNECTION_TIMEOUT_SECONDS,
        ssl_timeout: CONNECTION_TIMEOUT_SECONDS,
      )

      if subscription.first_error_at || subscription.error_count != 0
        subscription.update_columns(error_count: 0, first_error_at: nil)
      end

      DiscourseEvent.trigger(:push_notification_sent, user, message)
    rescue WebPush::ExpiredSubscription
      subscription.destroy!
    rescue WebPush::ResponseError => e
      if e.response.message == "MismatchSenderId"
        subscription.destroy!
      else
        handle_generic_error(subscription, e, user, endpoint, message)
      end
    rescue Timeout::Error => e
      handle_generic_error(subscription, e, user, endpoint, message)
    rescue OpenSSL::SSL::SSLError => e
      handle_generic_error(subscription, e, user, endpoint, message)
    end
  end

  private_class_method :send_notification
  private_class_method :handle_generic_error
end