# frozen_string_literal: true

class Stylesheet::Manager::Builder
  attr_reader :theme

  def initialize(target: :desktop, theme: nil, color_scheme: nil, manager:)
    @target = target
    @theme = theme
    @color_scheme = color_scheme
    @manager = manager
  end

  def compile(opts = {})
    if !opts[:force]
      if File.exist?(stylesheet_fullpath)
        if !StylesheetCache.where(target: qualified_target, digest: digest).exists?
          begin
            source_map =
              begin
                File.read(source_map_fullpath)
              rescue Errno::ENOENT
              end

            StylesheetCache.add(
              qualified_target,
              digest,
              File.read(stylesheet_fullpath),
              source_map,
            )
          rescue => e
            Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}"
          end
        end
        return true
      end
    end

    rtl = @target.to_s =~ /_rtl\z/
    css, source_map =
      with_load_paths do |load_paths|
        Stylesheet::Compiler.compile_asset(
          @target,
          rtl: rtl,
          theme_id: theme&.id,
          theme_variables: theme&.scss_variables.to_s,
          source_map_file: source_map_url_relative_from_stylesheet,
          color_scheme_id: @color_scheme&.id,
          load_paths: load_paths,
        )
      rescue SassC::SyntaxError, SassC::NotRenderedError => e
        if Stylesheet::Importer::THEME_TARGETS.include?(@target.to_s)
          # no special errors for theme, handled in theme editor
          ["", nil]
        elsif @target.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET
          # log error but do not crash for errors in color definitions SCSS
          Rails.logger.error "SCSS compilation error: #{e.message}"
          ["", nil]
        else
          raise Discourse::ScssError, e.message
        end
      end

    FileUtils.mkdir_p(cache_fullpath)

    File.open(stylesheet_fullpath, "w") { |f| f.puts css }

    File.open(source_map_fullpath, "w") { |f| f.puts source_map } if source_map.present?

    begin
      StylesheetCache.add(qualified_target, digest, css, source_map)
    rescue => e
      Rails.logger.warn "Completely unexpected error adding item to cache #{e}"
    end
    css
  end

  def current_hostname
    Discourse.current_hostname
  end

  def cache_fullpath
    Stylesheet::Manager.cache_fullpath
  end

  def stylesheet_fullpath
    "#{cache_fullpath}/#{stylesheet_filename}"
  end

  def source_map_fullpath
    "#{cache_fullpath}/#{source_map_filename}"
  end

  def source_map_filename
    "#{stylesheet_filename}.map"
  end

  def source_map_url_relative_from_stylesheet
    "#{source_map_filename}?__ws=#{current_hostname}"
  end

  def stylesheet_fullpath_no_digest
    "#{cache_fullpath}/#{stylesheet_filename_no_digest}"
  end

  def stylesheet_absolute_url
    "#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{current_hostname}"
  end

  def root_path
    "#{GlobalSetting.relative_url_root}/"
  end

  def stylesheet_relpath
    "#{root_path}stylesheets/#{stylesheet_filename}"
  end

  def stylesheet_relpath_no_digest
    "#{root_path}stylesheets/#{stylesheet_filename_no_digest}"
  end

  def qualified_target
    if is_theme?
      "#{@target}_#{theme&.id}"
    elsif @color_scheme
      "#{@target}_#{scheme_slug}_#{@color_scheme&.id.to_s}_#{@theme&.id}"
    else
      scheme_string = theme&.color_scheme ? "_#{theme.color_scheme.id}" : ""
      "#{@target}#{scheme_string}"
    end
  end

  def stylesheet_filename(with_digest = true)
    digest_string = "_#{self.digest}" if with_digest
    "#{qualified_target}#{digest_string}.css"
  end

  def stylesheet_filename_no_digest
    stylesheet_filename(_with_digest = false)
  end

  def is_theme?
    !!(@target.to_s =~ Stylesheet::Manager::THEME_REGEX)
  end

  def is_color_scheme?
    !!(@target.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET)
  end

  def scheme_slug
    Slug.for(ActiveSupport::Inflector.transliterate(@color_scheme.name), "scheme")
  end

  # digest encodes the things that trigger a recompile
  def digest
    @digest ||=
      begin
        if is_theme?
          theme_digest
        elsif is_color_scheme?
          color_scheme_digest
        else
          default_digest
        end
      end
  end

  def with_load_paths
    if theme
      theme.with_scss_load_paths { |p| yield p }
    else
      yield nil
    end
  end

  def scss_digest
    if %i[mobile_theme desktop_theme].include?(@target)
      resolve_baked_field(@target.to_s.sub("_theme", ""), :scss)
    elsif @target == :embedded_theme
      resolve_baked_field(:common, :embedded_scss)
    else
      raise "attempting to look up theme digest for invalid field"
    end
  end

  def theme_digest
    Digest::SHA1.hexdigest(
      scss_digest.to_s + color_scheme_digest.to_s + settings_digest + uploads_digest +
        current_hostname,
    )
  end

  # this protects us from situations where new versions of a plugin removed a file
  # old instances may still be serving CSS and not aware of the change
  # so we could end up poisoning the cache with a bad file that can not be removed
  def plugins_digest
    assets = []
    DiscoursePluginRegistry.stylesheets.each { |_, paths| assets += paths.to_a }
    DiscoursePluginRegistry.mobile_stylesheets.each { |_, paths| assets += paths.to_a }
    DiscoursePluginRegistry.desktop_stylesheets.each { |_, paths| assets += paths.to_a }
    Digest::SHA1.hexdigest(assets.sort.join)
  end

  def settings_digest
    themes =
      if !theme
        []
      elsif Theme.is_parent_theme?(theme.id)
        @manager.load_themes(@manager.theme_ids)
      else
        [@manager.get_theme(theme.id)]
      end

    fields =
      themes.each_with_object([]) do |theme, array|
        array.concat(theme.yaml_theme_fields.map(&:updated_at))
      end

    settings =
      themes.each_with_object([]) do |theme, array|
        array.concat(theme.theme_settings.map(&:updated_at))
      end

    timestamps = fields.concat(settings).map!(&:to_f).sort!.join(",")

    Digest::SHA1.hexdigest(timestamps)
  end

  def uploads_digest
    sha1s = []

    (theme&.upload_fields || []).map { |upload_field| sha1s << upload_field.upload&.sha1 }

    Digest::SHA1.hexdigest(sha1s.compact.sort!.join("\n"))
  end

  def default_digest
    Digest::SHA1.hexdigest "default-#{Stylesheet::Manager.fs_asset_cachebuster}-#{plugins_digest}-#{current_hostname}"
  end

  def color_scheme_digest
    cs = @color_scheme || theme&.color_scheme

    categories_updated =
      Stylesheet::Manager
        .cache
        .defer_get_set("categories_updated") do
          Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum
        end

    fonts = "#{SiteSetting.base_font}-#{SiteSetting.heading_font}"

    digest_string = "#{current_hostname}-"
    if cs || categories_updated > 0
      theme_color_defs = resolve_baked_field(:common, :color_definitions)
      digest_string +=
        "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{categories_updated}-#{fonts}"
    else
      digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"

      if cdn_url = GlobalSetting.cdn_url
        digest_string += "-#{cdn_url}"
      end
    end
    Digest::SHA1.hexdigest digest_string
  end

  def resolve_baked_field(target, name)
    theme_ids =
      if !theme
        []
      elsif Theme.is_parent_theme?(theme.id)
        @manager.theme_ids
      else
        [theme.id]
      end

    theme_ids = [theme_ids.first] if name != :color_definitions

    baked_fields = []
    targets = [Theme.targets[target.to_sym], Theme.targets[:common]]

    @manager
      .load_themes(theme_ids)
      .each do |theme|
        theme.builder_theme_fields.each do |theme_field|
          if theme_field.name == name.to_s && targets.include?(theme_field.target_id)
            baked_fields << theme_field
          end
        end
      end

    baked_fields
      .map do |f|
        f.ensure_baked!
        f.value_baked || f.value
      end
      .join("\n")
  end
end
