# frozen_string_literal: true

module Vite
  # We use two manifest files to list the assets loaded in our backend:
  # - manifest.json: This is the main file that lists all the javascript and stylesheets our frontend app uses
  # - manifest-assets.json: This only lists media assets linked directly from our backend, mainly images
  #
  # Not all the assets listed in the manifest.json file are linked or used by the app view, so we don't need to
  # keep track of those. The idea behind this class is to keep track only of the main entrypoints and all their
  # subsequent imports. Imports in this case include other javascript modules and stylesheets generated by Vite.
  #
  # This class focuses on reducing the amount of data we keep per entrypoint and making fast lookups. To do so
  # it keeps two main datastructures:
  # - entries: A Hash with references to each main entrypoint (including media assets)
  # - pool: An Array with all the referenced imports and stylesheets
  #
  # Each entrypoint has a list of IDs for each import and stylesheet that it needs to load, this ID is
  # an index to the main pool. With this the referenced asset is stored only once and referenced across
  # entries with a unique ID. In the original manifest files references are also nested, if an entrypoint
  # imports a javascript module, this first module can import other modules and stylesheet and so on. To keep
  # fetching entrypoints fast, the final list of modules and stylesheets to load per entrypoint
  # is expanded when loading the manifest data and not when rendering views. Each entrypoint has the final
  # list of IDs of imports and stylesheets it will need to link in the backend, without further recursive
  # checks.
  class Manifest
    class MissingManifestError < StandardError; end

    class MissingEntryError < StandardError
      def initialize(entry)
        super("Failed to find entry '#{entry}' in manifest files")
      end
    end

    Entrypoint = Struct.new(:file, :integrity, :import_ids, :stylesheet_ids)
    Asset = Struct.new(:file, :integrity)

    attr_reader :config, :entries, :pool, :logger

    def initialize(config:, logger:)
      @config = config
      @virtual = {}
      @entries = {}
      @pool = []
      @loaded = false
      @logger = logger
    end

    def reload
      @loaded = false
      load
    end

    def load
      return if @loaded

      retries = 0

      begin
        build if retries.positive?
        load_manifest
        @loaded = true
      rescue Errno::ENOENT => e
        if retries.zero? && config.auto_build?
          retries = 1
          retry
        else
          raise MissingManifestError, e.message
        end
      end
    end

    def fetch(name, type: nil)
      load

      key = type == :virtual ? @virtual[name] : name

      entries[key]
    end

    def fetch!(name, type: nil)
      fetch(name, type:) || raise(MissingEntryError, name)
    end

    def imports_for(entry)
      find_entries(entry[:import_ids] || [])
    end

    def stylesheets_for(entry)
      find_entries(entry[:stylesheet_ids] || [])
    end

    private

    def load_manifest
      @virtual = {}
      @entries = {}
      @pool = []
      # intermediate Hash for fast lookups while parsing the manifest
      @lookup = {}

      logger.debug { "loading manifest file: #{config.manifest_path}" }
      json = JSON.load_file(config.manifest_path)
      json.each do |key, raw|
        # Only select main entrypoints and locales
        next unless raw['isEntry'] || key.start_with?('mastodon/locales/')

        import_ids, stylesheet_ids = resolve_relations(json, raw)

        @virtual[raw['name']] = key
        @entries[key] = Entrypoint.new(
          raw['file'],
          raw['integrity'],
          import_ids,
          stylesheet_ids
        )
      end

      logger.debug { "loading manifest file: #{config.manifest_assets_path}" }
      assets = JSON.load_file(config.manifest_assets_path)
      assets.each do |key, raw|
        @entries[key] = Asset.new(
          raw['file'],
          raw['integrity']
        )
      end
    ensure
      @lookup = {}
    end

    def build
      logger.debug { 'AutoBuild enabled' }
      builder.build
    end

    def builder
      @builder ||= Vite::Builder.new(config:, logger:)
    end

    def resolve_relations(manifest, raw)
      # NOTE: By using Set we make sure we add each asset only once
      imports = Set.new
      stylesheets = Set.new

      raw['imports']&.each do |import|
        raw_import = manifest[import]
        next unless raw_import

        id, deps = lookup(file: raw_import['file'], integrity: raw_import['integrity']) do
          resolve_relations(manifest, raw_import)
        end

        imports.merge(deps[0])
        stylesheets.merge(deps[1])
        imports.add id
      end

      raw['css']&.each do |stylesheet|
        # the css field includes the file names of the stylesheets and not
        # the src names which are the ones used as keys in the manifest
        raw_style = manifest.values.find { |value| value['file'] == stylesheet }
        # some listed CSS files are not included as a entry in the manifest,
        # but they are among the generated assets. We can still link to them
        # but without the integrity field
        raw_style = { 'file' => stylesheet } if raw_style.nil?

        id, = lookup(file: raw_style['file'], integrity: raw_style['integrity'])
        stylesheets.add id
      end

      [imports, stylesheets]
    end

    # Lookup in the internal cache if an entry has been processed
    # If not, yield and save the result of the block as the entry deps
    def lookup(file:, integrity:)
      id, deps = @lookup[file] || []

      if id.nil?
        @pool << Asset.new(file, integrity)
        id = @pool.size - 1 # last ID
        deps = yield if block_given?
        @lookup[file] = [id, deps]
      end

      [id, deps]
    end

    def find_entries(ids)
      ids.filter_map { |id| pool[id] }
    end
  end
end
