# coding: utf-8
# frozen_string_literal: true
require "current_user"
require "canonical_url"

module ApplicationHelper
  include CurrentUser
  include CanonicalURL::Helpers
  include ConfigurableUrls
  include GlobalPath

  def self.extra_body_classes
    @extra_body_classes ||= Set.new
  end

  # This generated equivalent of Ember's config/environment.js is used
  # in development, production, and theme tests. (i.e. everywhere except
  # regular tests)
  def discourse_config_environment(testing: false)
    # TODO: Can this come from Ember CLI somehow?
    config = {
      modulePrefix: "discourse",
      environment: Rails.env,
      rootURL: Discourse.base_path,
      locationType: "history",
      EmberENV: {
        FEATURES: {
        },
        EXTEND_PROTOTYPES: false,
      },
      APP: {
        name: "discourse",
        version: "#{Discourse::VERSION::STRING} #{Discourse.git_version}",
        # LOG_RESOLVER: true,
        # LOG_ACTIVE_GENERATION: true,
        # LOG_TRANSITIONS: true,
        # LOG_TRANSITIONS_INTERNAL: true,
        # LOG_VIEW_LOOKUPS: true,
      },
    }

    if testing
      config[:environment] = "test"
      config[:locationType] = "none"
      config[:APP][:LOG_ACTIVE_GENERATION] = false
      config[:APP][:LOG_VIEW_LOOKUPS] = false
      config[:APP][:rootElement] = "#ember-testing"
      config[:APP][:autoboot] = false
    end

    config.to_json
  end

  def google_universal_analytics_json(ua_domain_name = nil)
    result = {}
    result[:cookieDomain] = ua_domain_name.gsub(%r{\Ahttp(s)?://}, "") if ua_domain_name
    result[:userId] = current_user.id if current_user.present?
    result[:allowLinker] = true if SiteSetting.ga_universal_auto_link_domains.present?
    result.to_json
  end

  def ga_universal_json
    google_universal_analytics_json(SiteSetting.ga_universal_domain_name)
  end

  def google_tag_manager_json
    google_universal_analytics_json
  end

  def csp_nonce_placeholder
    ContentSecurityPolicy.nonce_placeholder(response.headers, request_env: request.env)
  end

  def track_view_session_id_placeholder
    response.headers[
      ::Middleware::TrackViewSessionIdInjector::PLACEHOLDER_HEADER
    ] ||= "[[track_view_session_id_placeholder_#{SecureRandom.hex}]]"
  end

  def shared_session_key
    if SiteSetting.long_polling_base_url != "/" && current_user
      sk = "shared_session_key"
      return request.env[sk] if request.env[sk]

      token = request.env[Auth::DefaultCurrentUserProvider::USER_TOKEN_KEY]
      return if !token || token.user != current_user

      request.env[sk] = key = (session[sk] ||= SecureRandom.hex)
      Auth::DefaultCurrentUserProvider.store_shared_session_key(key, token.id.to_s)
      key
    end
  end

  def is_brotli_req?
    request.env["HTTP_ACCEPT_ENCODING"] =~ /br/
  end

  def is_gzip_req?
    request.env["HTTP_ACCEPT_ENCODING"] =~ /gzip/
  end

  def generate_import_map(plugin_assets)
    imports =
      plugin_assets
        .filter { it[:importmap_name] }
        .map { [it[:importmap_name], script_asset_path(it[:name])] }
        .to_h

    available_plugins = plugin_assets.map { |a| a[:plugin].directory_name }
    external_plugin_imports =
      (
        plugin_assets.flat_map { |a| a[:external_plugin_imports] || [] } +
          theme_js_assets.flat_map { |a| a[:external_plugin_imports] }
      ).uniq

    external_plugin_imports.each do |plugin_name|
      if available_plugins.include?(plugin_name)
        imports["discourse/plugins/#{plugin_name}?"] = imports["discourse/plugins/#{plugin_name}"]
      else
        imports["discourse/plugins/#{plugin_name}?"] = Plugin::JsManager.optional_plugin_stub
        imports["discourse/plugins/#{plugin_name}"] = Plugin::JsManager.required_plugin_stub(
          plugin_name,
        )
      end
    end

    JSON.pretty_generate({ imports: }).html_safe
  end

  def script_asset_path(script)
    path = ActionController::Base.helpers.asset_path("#{script}.js")

    if GlobalSetting.use_s3? && GlobalSetting.s3_cdn_url
      resolved_s3_asset_cdn_url =
        GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url
      if GlobalSetting.cdn_url
        folder = ActionController::Base.config.relative_url_root || "/"
        path =
          path.gsub(
            File.join(GlobalSetting.cdn_url, folder, "/"),
            File.join(resolved_s3_asset_cdn_url, "/"),
          )
      else
        # we must remove the subfolder path here, assets are uploaded to s3
        # without it getting involved
        if ActionController::Base.config.relative_url_root
          path = path.sub(ActionController::Base.config.relative_url_root, "")
        end

        path = "#{resolved_s3_asset_cdn_url}#{path}"
      end

      if is_brotli_req?
        path = path.sub("/assets/js/", "/assets/br/")
      elsif is_gzip_req?
        path = path.sub("/assets/js/", "/assets/gz/")
      end
    end

    path
  end

  def preload_script(script, type_module: false, attrs: {})
    resolved_script = EmberAssets.script_chunks[script]&.first || script
    path = script_asset_path(resolved_script)
    preload_script_url(path, entrypoint: script, type_module:, attrs:).html_safe
  end

  def module_preloads_for(*scripts)
    resolved_preload_scripts =
      scripts.compact.flat_map { |script| EmberAssets.script_chunks[script] }.compact.uniq

    modulepreload_tags = resolved_preload_scripts.map { |script| <<~HTML }
      <link rel="modulepreload" href="#{script_asset_path script}" nonce="#{csp_nonce_placeholder}">
    HTML

    modulepreload_tags.join("\n").html_safe
  end

  def preload_script_url(url, entrypoint: nil, type_module: false, attrs: nil)
    entrypoint_attribute = entrypoint ? "data-discourse-entrypoint=\"#{entrypoint}\"" : ""
    nonce_attribute = "nonce=\"#{csp_nonce_placeholder}\""

    extra_attrs =
      attrs&.map { |k, v| "#{ERB::Util.html_escape(k)}=\"#{ERB::Util.html_escape(v)}\"" }&.join(" ")
    extra_attrs = " #{extra_attrs}" if extra_attrs.present?

    add_resource_preload_list(url, "script")

    <<~HTML.html_safe
      <script #{type_module ? 'type="module"' : "defer"} src="#{url}" #{entrypoint_attribute}#{extra_attrs} #{nonce_attribute}></script>
    HTML
  end

  def add_resource_preload_list(resource_url, type)
    links =
      controller.instance_variable_get(:@asset_preload_links) ||
        controller.instance_variable_set(:@asset_preload_links, [])
    links << %Q(<#{resource_url}>; rel="preload"; as="#{type}")
  end

  def discourse_csrf_tags
    # anon can not have a CSRF token cause these are all pages
    # that may be cached, causing a mismatch between session CSRF
    # and CSRF on page and horrible impossible to debug login issues
    csrf_meta_tags if current_user
  end

  def html_classes
    list = []
    list << "rtl" if rtl?
    list << text_size_class
    list << "anon" unless current_user
    list << @embed_class if @embed_class.present?
    list.join(" ")
  end

  def body_classes
    result = ApplicationHelper.extra_body_classes.to_a

    result << "category-#{@category.slug_path.join("-")}" if @category && @category.url.present?

    if current_user.present? && current_user.primary_group_id &&
         primary_group_name = Group.where(id: current_user.primary_group_id).pick(:name)
      result << "primary-group-#{primary_group_name.downcase}"
    end

    result.join(" ")
  end

  def text_size_class
    requested_cookie_size, cookie_seq = cookies[:text_size]&.split("|")
    server_seq = current_user&.user_option&.text_size_seq
    if cookie_seq && server_seq && cookie_seq.to_i >= server_seq &&
         UserOption.text_sizes.keys.include?(requested_cookie_size&.to_sym)
      cookie_size = requested_cookie_size
    end

    size = cookie_size || current_user&.user_option&.text_size || SiteSetting.default_text_size
    "text-size-#{size}"
  end

  def escape_unicode(javascript)
    if javascript
      javascript = javascript.scrub
      javascript.gsub!(/\342\200\250/u, "&#x2028;")
      javascript.gsub!(%r{(</)}u, '\u003C/')
      javascript
    else
      ""
    end
  end

  def format_topic_title(title)
    PrettyText.unescape_emoji strip_tags(title)
  end

  def with_format(format, &block)
    old_formats = formats
    self.formats = [format]
    block.call
    self.formats = old_formats
    nil
  end

  def age_words(secs)
    AgeWords.age_words(secs)
  end

  def short_date(dt)
    if dt.year == Time.now.year
      I18n.l(dt, format: :short_no_year)
    else
      I18n.l(dt, format: :date_only)
    end
  end

  def guardian
    @guardian ||= Guardian.new(current_user)
  end

  def admin?
    current_user.try(:admin?)
  end

  def moderator?
    current_user.try(:moderator?)
  end

  def staff?
    current_user.try(:staff?)
  end

  def rtl?
    Rtl::LOCALES.include? I18n.locale.to_s
  end

  def html_lang
    (request ? I18n.locale.to_s : SiteSetting.default_locale).sub("_", "-")
  end

  def title_content
    DiscoursePluginRegistry.apply_modifier(
      :meta_data_content,
      content_for(:title) || SiteSetting.title,
      :title,
      { url: request.fullpath },
    )
  end

  def description_content
    DiscoursePluginRegistry.apply_modifier(
      :meta_data_content,
      @description_meta || SiteSetting.site_description,
      :description,
      { url: request.fullpath },
    )
  end

  def is_crawler_homepage?
    request.path == "/" && use_crawler_layout?
  end

  # Creates open graph and twitter card meta data
  def crawlable_meta_data(opts = nil)
    opts ||= {}
    opts[:url] ||= "#{Discourse.base_url_no_prefix}#{request.fullpath}"

    # if slug generation method is encoded, non encoded urls can sneak in
    # via bots
    url = opts[:url]
    if url.encoding.name != "UTF-8" || !url.valid_encoding?
      opts[:url] = url.dup.force_encoding("UTF-8").scrub!
    end

    if opts[:image].blank?
      x_summary_large_image_url = SiteSetting.site_x_summary_large_image_url

      opts[:x_summary_large_image] = x_summary_large_image_url if x_summary_large_image_url.present?

      opts[:image] = SiteSetting.site_opengraph_image_url
    end

    # Use the correct scheme for opengraph/twitter image
    opts[:image] = get_absolute_image_url(opts[:image]) if opts[:image].present?
    opts[:x_summary_large_image] = get_absolute_image_url(opts[:x_summary_large_image]) if opts[
      :x_summary_large_image
    ].present?

    result = []
    result << tag(:meta, property: "og:site_name", content: opts[:site_name] || SiteSetting.title)
    result << tag(:meta, property: "og:type", content: "website")

    generate_twitter_card_metadata(result, opts)

    if opts[:image].present?
      result << tag(:meta, property: "og:image", content: opts[:image])
      if opts[:image_width].present? && opts[:image_height].present?
        result << tag(:meta, property: "og:image:width", content: opts[:image_width])
        result << tag(:meta, property: "og:image:height", content: opts[:image_height])
      end
      if opts[:image_type].present?
        result << tag(:meta, property: "og:image:type", content: opts[:image_type])
      end
    end

    %i[url title description].each do |property|
      if opts[property].present?
        content = (property == :url ? opts[property] : gsub_emoji_to_unicode(opts[property]))
        content =
          DiscoursePluginRegistry.apply_modifier(:meta_data_content, content, property, opts)
        result << tag(:meta, { property: "og:#{property}", content: content }, nil, true)
        result << tag(:meta, { name: "twitter:#{property}", content: content }, nil, true)
      end
    end
    Array
      .wrap(opts[:breadcrumbs])
      .each do |breadcrumb|
        result << tag(:meta, property: "og:article:section", content: breadcrumb[:name])
        result << tag(:meta, property: "og:article:section:color", content: breadcrumb[:color])
      end
    Array
      .wrap(opts[:tags])
      .each { |tag_name| result << tag(:meta, property: "og:article:tag", content: tag_name) }

    if opts[:read_time] && opts[:read_time] > 0 && opts[:like_count] && opts[:like_count] > 0
      result << tag(:meta, name: "twitter:label1", value: I18n.t("reading_time"))
      result << tag(
        :meta,
        name: "twitter:data1",
        value: I18n.t("reading_time_minutes", count: opts[:read_time]),
      )
      result << tag(:meta, name: "twitter:label2", value: I18n.t("likes"))
      result << tag(
        :meta,
        name: "twitter:data2",
        value: I18n.t("likes_count", count: opts[:like_count]),
      )
    end

    if opts[:published_time]
      result << tag(:meta, property: "article:published_time", content: opts[:published_time])
    end

    result << tag(:meta, property: "og:ignore_canonical", content: true) if opts[:ignore_canonical]

    result.join("\n")
  end

  def render_sitelinks_search_tag
    return if !guardian.can_search?

    if current_page?("/") || current_page?(Discourse.base_path)
      json = {
        "@context" => "http://schema.org",
        "@type" => "WebSite",
        :url => Discourse.base_url,
        :name => SiteSetting.title,
        :potentialAction => {
          "@type" => "SearchAction",
          :target => "#{Discourse.base_url}/search?q={search_term_string}",
          "query-input" => "required name=search_term_string",
        },
      }
      content_tag(:script, MultiJson.dump(json).html_safe, type: "application/ld+json")
    end
  end

  def discourse_pageview_tracking_meta_tags
    if !SiteSetting.trigger_browser_pageview_events && !SiteSetting.persist_browser_pageview_events
      return ""
    end
    return "" if Rails.env.development? && ENV["TRACK_REQUESTS"].blank?

    tags = +""
    tags << tag.meta(
      name: "discourse-track-view-session-id",
      content: track_view_session_id_placeholder,
    )
    if UpcomingChanges.enabled?(:dashboard_improvements)
      tags << tag.meta(name: "discourse-beacon-pageview-enabled", content: "true")
    end

    if SiteSetting.persist_browser_pageview_events
      tags << tag.meta(name: "discourse-engagement-tracking-enabled", content: "true")
    end
    tags.html_safe
  end

  def gsub_emoji_to_unicode(str)
    Emoji.gsub_emoji_to_unicode(str)
  end

  def application_logo_url
    @application_logo_url ||=
      if mobile_device?
        if dark_color_scheme? && SiteSetting.site_mobile_logo_dark_url.present?
          SiteSetting.site_mobile_logo_dark_url
        elsif SiteSetting.site_mobile_logo_url.present?
          SiteSetting.site_mobile_logo_url
        end
      else
        if dark_color_scheme? && SiteSetting.site_logo_dark_url.present?
          SiteSetting.site_logo_dark_url
        else
          SiteSetting.site_logo_url
        end
      end
  end

  def application_logo_dark_url
    @application_logo_dark_url ||=
      if dark_scheme_id != -1
        if mobile_device? && SiteSetting.site_mobile_logo_dark_url != application_logo_url
          SiteSetting.site_mobile_logo_dark_url
        elsif !mobile_device? && SiteSetting.site_logo_dark_url != application_logo_url
          SiteSetting.site_logo_dark_url
        end
      end
  end

  def waving_hand_url
    UrlHelper.cook_url(Emoji.url_for(":wave:t#{rand(2..6)}:"))
  end

  def login_path
    "#{Discourse.base_path}/login"
  end

  def mobile_device?
    MobileDetection.mobile_device?(request.user_agent)
  end

  def crawler_layout?
    controller&.use_crawler_layout?
  end

  def include_crawler_content?
    if current_user && !crawler_layout?
      params.key?(:print)
    else
      return false if !current_user && SiteSetting.login_required?

      crawler_layout? || !mobile_device? || !modern_mobile_device?
    end
  end

  def modern_mobile_device?
    MobileDetection.modern_mobile_device?(request.user_agent)
  end

  def customization_disabled?
    request.env[ApplicationController::NO_THEMES]
  end

  def include_ios_native_app_banner?
    current_user && current_user.trust_level >= 1 && SiteSetting.native_app_install_banner_ios
  end

  def ios_app_argument
    # argument only makes sense for DiscourseHub app
    if SiteSetting.ios_app_id == "1173672076"
      ", app-argument=discourse://new?siteUrl=#{Discourse.base_url}"
    else
      ""
    end
  end

  def custom_splash_screen_enabled?
    @custom_splash_screen_enabled ||= SiteSetting.splash_screen_image.is_a?(Upload)
  end

  def splash_screen_image_animated?
    build_splash_screen_image unless defined?(@splash_screen_image_svg)
    @splash_screen_image_svg.present? && @splash_screen_image_svg.match?(/@keyframes\s/)
  end

  def splash_screen_inline_svg
    build_splash_screen_image unless defined?(@splash_screen_image_svg)
    @splash_screen_image_svg&.html_safe
  end

  def splash_screen_image_data_uri(dark: false)
    build_splash_screen_image unless defined?(@splash_screen_image_svg)
    return nil if @splash_screen_image_svg.blank?

    # Replace CSS variable references with actual theme colors
    svg_with_colors = @splash_screen_image_svg.dup

    color_method = dark ? :dark_color_hex_for_name : :light_color_hex_for_name
    primary = "##{public_send(color_method, "primary")}"
    secondary = "##{public_send(color_method, "secondary")}"
    tertiary = "##{public_send(color_method, "tertiary")}"

    svg_with_colors.gsub!(/var\(\s*--primary\s*\)/, primary)
    svg_with_colors.gsub!(/var\(\s*--secondary\s*\)/, secondary)
    svg_with_colors.gsub!(/var\(\s*--tertiary\s*\)/, tertiary)

    # Use base64 encoding for better compatibility with complex SVGs
    "data:image/svg+xml;base64,#{Base64.strict_encode64(svg_with_colors)}"
  end

  private

  def generate_twitter_card_metadata(result, opts)
    img_url = opts[:x_summary_large_image].presence || opts[:image]

    # Twitter does not allow SVGs, see https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup
    if img_url.ends_with?(".svg")
      img_url = SiteSetting.site_logo_url.ends_with?(".svg") ? nil : SiteSetting.site_logo_url
    end

    if opts[:x_summary_large_image].present? && img_url.present?
      result << tag(:meta, name: "twitter:card", content: "summary_large_image")
      result << tag(:meta, name: "twitter:image", content: img_url)
    elsif opts[:image].present? && img_url.present?
      result << tag(:meta, name: "twitter:card", content: "summary")
      result << tag(:meta, name: "twitter:image", content: img_url)
    else
      result << tag(:meta, name: "twitter:card", content: "summary")
    end
  end

  def build_splash_screen_image
    @splash_screen_image_svg = nil

    upload = SiteSetting.splash_screen_image
    return unless upload.is_a?(Upload)

    @splash_screen_image_svg =
      Discourse
        .cache
        .fetch("splash_screen_svg_#{upload.id}_#{upload.sha1}", expires_in: 1.day) do
          upload.content.presence
        rescue StandardError => e
          Discourse.warn_exception(e, message: "Failed to fetch splash screen logo SVG")
          nil
        end
  end

  public

  def allow_plugins?
    !request.env[ApplicationController::NO_PLUGINS]
  end

  def allow_third_party_plugins?
    allow_plugins? && !request.env[ApplicationController::NO_UNOFFICIAL_PLUGINS]
  end

  def normalized_safe_mode
    safe_mode = []

    safe_mode << ApplicationController::NO_THEMES if customization_disabled?
    safe_mode << ApplicationController::NO_PLUGINS if !allow_plugins?
    safe_mode << ApplicationController::NO_UNOFFICIAL_PLUGINS if !allow_third_party_plugins?

    safe_mode.join(",")
  end

  def loading_admin?
    return false unless defined?(controller)
    return false if controller.class.name.blank?

    controller.class.name.split("::").first == "Admin"
  end

  def category_badge(category, opts = nil)
    CategoryBadge.html_for(category, opts).html_safe
  end

  SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES = [Rails.root.join("plugins/").to_s]
  private_constant :SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES

  if Rails.env.test?
    SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES << Rails.root.join("spec/fixtures/plugins/").to_s
  end

  SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES =
    SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES.each_with_object({}) do |plugins_prefix, connectors|
      Dir
        .glob("#{plugins_prefix}*/app/views/connectors/**/*.html.erb")
        .each do |template_path|
          template_path =~ Regexp.new("/connectors/(.*)/.*\.html\.erb$")
          outlet_name = Regexp.last_match(1)
          connectors[outlet_name] ||= []

          connectors[outlet_name] << begin
            ActionView::Template.new(
              File.read(template_path),
              "discourse_plugin_outlet__#{name}",
              ActionView::Template.handler_for_extension("erb"),
              locals: [],
              format: :html,
              virtual_path: template_path,
            )
          end
        end
    end
  private_constant :SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES

  def server_plugin_outlet(name, locals: {})
    return "" if !GlobalSetting.load_plugins?
    return "" if !SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES.key?(name)

    SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES[name]
      .map { |template| render template:, locals: }
      .join
      .html_safe
  end

  def topic_featured_link_domain(link)
    uri = UrlHelper.encode_and_parse(link)
    uri = URI.parse("http://#{uri}") if uri.scheme.nil?
    host = uri.host.downcase
    host.start_with?("www.") ? host[4..-1] : host
  rescue StandardError
    ""
  end

  def theme_id
    if customization_disabled?
      nil
    else
      request.env[:resolved_theme_id]
    end
  end

  def stylesheet_manager
    return @stylesheet_manager if defined?(@stylesheet_manager)
    @stylesheet_manager = Stylesheet::Manager.new(theme_id: theme_id)
  end

  def user_scheme_id
    return @user_scheme_id if defined?(@user_scheme_id)
    scheme_id = cookies[:color_scheme_id] || current_user&.user_option&.color_scheme_id

    @user_scheme_id = ColorScheme.valid_id(scheme_id) if ColorScheme.exists?(
      id: scheme_id,
      user_selectable: true,
    ) || theme&.color_scheme_id == scheme_id.to_i
  end

  def scheme_id
    return @scheme_id if defined?(@scheme_id)

    if user_scheme_id
      return user_scheme_id unless theme_limits_color_schemes?
      return user_scheme_id if ColorScheme.exists?(id: user_scheme_id, theme_id: theme_id)
    end

    return if theme_id.blank?

    @scheme_id = Theme.where(id: theme_id).pick(:color_scheme_id)
  end

  def theme
    @theme = theme_id ? Theme.find_by_id(theme_id) : Theme.find_default
  end

  def user_dark_scheme_id
    return @user_dark_scheme_id if defined?(@user_dark_scheme_id)
    scheme_id = cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id

    @user_dark_scheme_id = ColorScheme.valid_id(scheme_id) if ColorScheme.exists?(
      id: scheme_id,
      user_selectable: true,
    ) || theme&.dark_color_scheme_id == scheme_id.to_i
  end

  def dark_scheme_id
    if user_dark_scheme_id
      return user_dark_scheme_id unless theme_limits_color_schemes?
      return user_dark_scheme_id if ColorScheme.exists?(id: user_dark_scheme_id, theme_id: theme_id)
    end

    theme = theme_id ? Theme.find_by_id(theme_id) : Theme.find_default
    dark_id = theme&.dark_color_scheme_id
    return dark_id if dark_id.present?
    return theme&.color_scheme_id if theme_limits_color_schemes?
    -1
  end

  def theme_limits_color_schemes?
    return @theme_limits_color_schemes if defined?(@theme_limits_color_schemes)
    @theme_limits_color_schemes =
      theme_id.present? &&
        ThemeModifierSet.exists?(theme_id: theme_id, only_theme_color_schemes: true)
  end

  def current_homepage
    current_user&.user_option&.homepage || HomepageHelper.resolve(request, current_user)
  end

  def build_plugin_html(name, **kwargs)
    return "" unless allow_plugins?
    DiscoursePluginRegistry.build_html(name, controller, **kwargs) || ""
  end

  def crawler_topic_container_schema(topic)
    tag.attributes(
      DiscoursePluginRegistry.apply_modifier(
        :topic_crawler_container_schema,
        { itemscope: true, itemtype: "http://schema.org/DiscussionForumPosting" },
        topic,
      ),
    )
  end

  def crawler_topic_main_entity_schema(topic)
    tag.attributes(
      DiscoursePluginRegistry.apply_modifier(:topic_crawler_main_entity_schema, {}, topic),
    ).presence
  end

  def crawler_post_schema_hash(post, topic)
    @crawler_post_schema_hash ||= {}
    @crawler_post_schema_hash[post.id] ||= begin
      default = {
        itemprop: "comment",
        itemscope: true,
        itemtype: "http://schema.org/Comment",
      } unless post.is_first_post?
      DiscoursePluginRegistry.apply_modifier(:topic_crawler_post_schema, default || {}, post, topic)
    end
  end

  def crawler_post_schema(post, topic)
    tag.attributes(crawler_post_schema_hash(post, topic))
  end

  def crawler_post_schema_overridden?(post, topic)
    hash = crawler_post_schema_hash(post, topic)
    itemprop = hash[:itemprop]
    (itemprop.present? && itemprop != "comment") || hash[:data].present?
  end

  def crawler_post_emits_microdata?(post, topic)
    post.is_first_post? || crawler_post_schema_hash(post, topic)[:itemscope]
  end

  def crawler_post_schema_skip?(post, topic)
    DiscoursePluginRegistry.apply_modifier(:topic_crawler_skip_post, false, post, topic)
  end

  # If there is plugin HTML return that, otherwise yield to the template
  def replace_plugin_html(name)
    if (html = build_plugin_html(name)).present?
      html
    else
      yield
      nil
    end
  end

  def theme_lookup(name)
    Theme.lookup_field(
      theme_id,
      mobile_device? ? :mobile : :desktop,
      name,
      skip_transformation: request.env[:skip_theme_ids_transformation].present?,
      csp_nonce: csp_nonce_placeholder,
    )
  end

  def theme_translations_lookup
    Theme.lookup_field(
      theme_id,
      :translations,
      I18n.locale,
      skip_transformation: request.env[:skip_theme_ids_transformation].present?,
      csp_nonce: csp_nonce_placeholder,
    )
  end

  def theme_js_assets
    Theme.js_asset_info(
      theme_id,
      skip_transformation: request.env[:skip_theme_ids_transformation].present?,
    )
  end

  def discourse_stylesheet_preload_tag(name, opts = {})
    manager =
      if opts.key?(:theme_id)
        Stylesheet::Manager.new(theme_id: customization_disabled? ? nil : opts[:theme_id])
      else
        stylesheet_manager
      end

    manager.stylesheet_preload_tag(name, "all")
  end

  def discourse_stylesheet_link_tag(name, opts = {})
    manager =
      if opts.key?(:theme_id)
        Stylesheet::Manager.new(theme_id: customization_disabled? ? nil : opts[:theme_id])
      else
        stylesheet_manager
      end

    name = :"#{name}_rtl" if opts[:supports_rtl] && rtl?

    manager.stylesheet_link_tag(name, opts[:media] || "all", method(:add_resource_preload_list))
  end

  def discourse_preload_color_scheme_stylesheets
    result = +""

    result << stylesheet_manager.color_scheme_stylesheet_preload_tag(
      scheme_id,
      fallback_to_base: true,
    )

    if dark_scheme_id != -1
      result << stylesheet_manager.color_scheme_stylesheet_preload_tag(
        dark_scheme_id,
        fallback_to_base: false,
      )
    end

    result.html_safe
  end

  def discourse_color_scheme_stylesheets
    light_href =
      stylesheet_manager.color_scheme_stylesheet_link_tag_href(scheme_id, fallback_to_base: true)
    add_resource_preload_list(light_href, "style")

    dark_href = nil
    if dark_scheme_id != -1
      dark_href =
        stylesheet_manager.color_scheme_stylesheet_link_tag_href(
          dark_scheme_id,
          fallback_to_base: false,
        )
    end

    result = +""
    if dark_href && dark_href != light_href
      add_resource_preload_list(dark_href, "style")

      result << color_scheme_stylesheet_link_tag(
        light_href,
        light_elements_media_query,
        "light-scheme",
        scheme_id,
      )
      result << color_scheme_stylesheet_link_tag(
        dark_href,
        dark_elements_media_query,
        "dark-scheme",
        dark_scheme_id,
      )
    else
      result << color_scheme_stylesheet_link_tag(light_href, "all", "light-scheme", scheme_id)
    end
    result.html_safe
  end

  def discourse_theme_color_meta_tags
    result = +""
    if dark_scheme_id != -1
      result << <<~HTML
        <meta name="theme-color" media="#{light_elements_media_query}" content="##{light_color_hex_for_name("header_background")}">
        <meta name="theme-color" media="#{dark_elements_media_query}" content="##{dark_color_hex_for_name("header_background")}">
      HTML
    else
      result << <<~HTML
        <meta name="theme-color" media="all" content="##{light_color_hex_for_name("header_background")}">
      HTML
    end
    result.html_safe
  end

  def discourse_color_scheme_meta_tag
    scheme =
      if dark_scheme_id == -1
        # no automatic client-side switching
        dark_color_scheme? ? "dark" : "light"
      else
        # auto-switched based on browser setting
        "light dark"
      end
    <<~HTML.html_safe
        <meta name="color-scheme" content="#{scheme}">
      HTML
  end

  def dark_color_scheme?
    return false if scheme_id.blank?
    ColorScheme.find_by_id(scheme_id)&.is_dark?
  end

  def forced_light_mode?
    return false if dark_color_scheme?

    cookie = cookies[:forced_color_mode]
    return cookie == "light" if cookie.present?

    !!current_user&.user_option&.light_mode_forced?
  end

  def forced_dark_mode?
    return false if dark_scheme_id == -1

    cookie = cookies[:forced_color_mode]
    return cookie == "dark" if cookie.present?

    !!current_user&.user_option&.dark_mode_forced?
  end

  def light_color_hex_for_name(name)
    ColorScheme.hex_for_name(name, scheme_id)
  end

  def dark_color_hex_for_name(name)
    ColorScheme.hex_for_name(name, dark_scheme_id)
  end

  def dark_elements_media_query
    if forced_light_mode?
      "none"
    elsif forced_dark_mode?
      "all"
    else
      "(prefers-color-scheme: dark)"
    end
  end

  def light_elements_media_query
    if forced_light_mode?
      "all"
    elsif forced_dark_mode?
      "none"
    else
      "(prefers-color-scheme: light)"
    end
  end

  def preloaded_json
    return "{}" if !@application_layout_preloader

    @application_layout_preloader
      .preloaded_data
      .transform_values { |value| escape_unicode(value) }
      .to_json
  end

  def client_side_setup_data
    setup_data = {
      cdn: Rails.configuration.action_controller.asset_host,
      base_url: Discourse.base_url,
      base_uri: Discourse.base_path,
      environment: Rails.env,
      letter_avatar_version: LetterAvatar.version,
      service_worker_url: "service-worker.js",
      default_locale: SiteSetting.default_locale,
      asset_version: Discourse.assets_digest,
      disable_custom_css: loading_admin?,
      highlight_js_path: HighlightJs.path,
      svg_sprite_path: SvgSprite.path(theme_id),
      enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
      color_scheme_is_dark: dark_color_scheme?,
      user_color_scheme_id: user_scheme_id || -1,
      user_dark_scheme_id: user_dark_scheme_id || -1,
      is_staff: staff?,
    }

    if Rails.env.development?
      setup_data[:svg_icon_list] = SvgSprite.all_icons(theme_id)

      setup_data[:debug_preloaded_app_data] = true if ENV["DEBUG_PRELOADED_APP_DATA"]
      setup_data[:mb_last_file_change_id] = MessageBus.last_id("/file-change")
    end

    if Rails.env.test?
      if ENV["CAPYBARA_PLAYWRIGHT_DEBUG_CLIENT_SETTLED"].present?
        setup_data[:capybara_playwright_debug_client_settled] = true
      end

      # Allow controlling deprecation behavior in tests via environment variable
      # Used to enforce or disable deprecation throwing for specific test runs
      if ENV["EMBER_RAISE_ON_DEPRECATION"] == "1"
        setup_data[:raise_on_deprecation] = true
      elsif ENV["EMBER_RAISE_ON_DEPRECATION"] == "0"
        setup_data[:raise_on_deprecation] = false
      end
    end

    if guardian.can_enable_safe_mode? && params["safe_mode"]
      setup_data[:safe_mode] = normalized_safe_mode
    end

    if SiteSetting.Upload.enable_s3_uploads
      setup_data[:s3_cdn] = SiteSetting.Upload.s3_cdn_url.presence
      setup_data[:s3_base_url] = SiteSetting.Upload.s3_base_url
    end

    setup_data
  end

  def get_absolute_image_url(link)
    absolute_url = link
    if link.start_with?("//")
      uri = URI(Discourse.base_url)
      absolute_url = "#{uri.scheme}:#{link}"
    elsif link.start_with?("/uploads/", "/images/", "/user_avatar/")
      absolute_url = "#{Discourse.base_url}#{link}"
    elsif GlobalSetting.relative_url_root && link.start_with?(GlobalSetting.relative_url_root)
      absolute_url = "#{Discourse.base_url_no_prefix}#{link}"
    end
    absolute_url
  end

  def escape_noscript(&block)
    raw capture(&block).gsub(%r{<(/\s*noscript)}i, '&lt;\1')
  end

  def manifest_url
    # If you want the `manifest_url` to be different for a specific action,
    # in the action set @manifest_url = X. Originally added for chat to add a
    # separate manifest
    @manifest_url || "#{Discourse.base_path}/manifest.webmanifest"
  end

  def can_sign_up?
    !@readonly_mode && SiteSetting.allow_new_registrations && !SiteSetting.invite_only &&
      !SiteSetting.enable_discourse_connect
  end

  def rss_creator(user)
    user&.display_name
  end

  def anonymous_top_menu_items
    Discourse.anonymous_top_menu_items.map(&:to_s)
  end

  def authentication_data
    return @authentication_data if defined?(@authentication_data)

    @authentication_data =
      begin
        value = cookies[:authentication_data]
        cookies.delete(:authentication_data, path: Discourse.base_path("/")) if value
        current_user ? nil : value
      end
  end

  def color_scheme_stylesheet_link_tag(href, media, css_class, scheme_id)
    scheme_id = Integer(scheme_id, exception: false)
    %[<link href="#{href}" media="#{media}" rel="stylesheet" class="#{css_class}"#{scheme_id && scheme_id != -1 ? %[ data-scheme-id="#{scheme_id}"] : ""}/>]
  end
end
