# frozen_string_literal: true

module PlatformosCheck
  class UndefinedObject < LiquidCheck
    category :liquid
    doc docs_url(__FILE__)
    severity :error

    class TemplateInfo
      def initialize(app_file: nil)
        @all_variable_lookups = {}
        @all_assigns = {}
        @all_captures = {}
        @all_forloops = {}
        @all_renders = {}
        @app_file = app_file
      end

      attr_reader :all_assigns, :all_captures, :all_forloops, :app_file, :all_renders

      def add_render(name:, node:)
        @all_renders[name] = node
      end

      def add_variable_lookup(name:, node:)
        parent = node
        line_number = nil
        loop do
          line_number = parent.line_number
          parent = parent.parent
          break unless line_number.nil? && parent
        end
        key = [name, line_number]
        @all_variable_lookups[key] = node
      end

      def all_variables
        all_assigns.keys + all_captures.keys + all_forloops.keys
      end

      def each_partial
        @all_renders.each do |(name, info)|
          yield [name, info]
        end
      end

      def each_variable_lookup(unique_keys = false)
        seen = Set.new
        @all_variable_lookups.each do |(key, info)|
          name, _line_number = key

          next if unique_keys && seen.include?(name)

          seen << name

          yield [key, info]
        end
      end
    end

    def self.single_file(**_args)
      true
    end

    def initialize(config_type: :default)
      @config_type = config_type
      @files = {}
    end

    def on_document(node)
      @files[node.app_file.name] = TemplateInfo.new(app_file: node.app_file)
    end

    def on_assign(node)
      @files[node.app_file.name].all_assigns[node.value.to] = node
    end

    def on_capture(node)
      @files[node.app_file.name].all_captures[node.value.instance_variable_get(:@to)] = node
    end

    def on_parse_json(node)
      @files[node.app_file.name].all_captures[node.value.to] = node
    end

    def on_for(node)
      @files[node.app_file.name].all_forloops[node.value.variable_name] = node
    end

    def on_include(_node)
      # NOOP: we purposely do nothing on `include` since it is deprecated
    end

    def on_render(node)
      return unless node.value.template_name_expr.is_a?(String)

      partial_name = node.value.template_name_expr
      @files[node.app_file.name].add_render(
        name: partial_name,
        node:
      )
    end

    def on_function(node)
      @files[node.app_file.name].all_assigns[node.value.to] = node

      return unless node.value.from.is_a?(String)

      @files[node.app_file.name].add_render(
        name: node.value.from,
        node:
      )
    end

    def on_graphql(node)
      @files[node.app_file.name].all_assigns[node.value.to] = node
    end

    def on_background(node)
      return unless node.value.partial_syntax

      @files[node.app_file.name].all_assigns[node.value.to] = node

      return unless node.value.partial_name.is_a?(String)

      @files[node.app_file.name].add_render(
        name: node.value.partial_name,
        node:
      )
    end

    def on_variable_lookup(node)
      @files[node.app_file.name].add_variable_lookup(
        name: node.value.name,
        node:
      )
    end

    def single_file_end_dependencies(app_file)
      @files[app_file.name].all_renders.keys.map do |partial_name|
        next if @files[partial_name]

        partial_file = @platformos_app.partials.detect { |p| p.name == partial_name } # NOTE: undefined partial

        next unless partial_file

        partial_file
      end.compact
    end

    def on_end
      all_global_objects = PlatformosCheck::PlatformosLiquid::Object.labels
      all_global_objects.freeze

      each_template do |(_name, info)|
        if info.app_file.notification?
          # NOTE: `data` comes from graphql for notifications
          check_object(info, all_global_objects + %w[data response form])
        elsif info.app_file.form?
          # NOTE: `data` comes from graphql for notifications
          check_object(info, all_global_objects + %w[form form_builder])
        else
          check_object(info, all_global_objects)
        end
      end
    end

    private

    attr_reader :config_type

    def each_template
      @files.each do |(name, info)|
        yield [name, info]
      end
    end

    def check_object(info, all_global_objects, render_node = nil, visited_partials = Set.new, level = 0)
      return if level > 1

      check_undefined(info, all_global_objects, render_node) unless info.app_file.partial? && render_node.nil? # ||

      info.each_partial do |(partial_name, node)|
        next if visited_partials.include?(partial_name)

        partial_info = @files[partial_name]

        next unless partial_info # NOTE: undefined partial

        partial_variables = node.value.attributes.keys +
                            [node.value.instance_variable_get(:@alias_name)]
        visited_partials << partial_name
        check_object(partial_info, all_global_objects + partial_variables, node, visited_partials, level + 1)
      end
    end

    def check_undefined(info, all_global_objects, render_node)
      all_variables = info.all_variables

      info.each_variable_lookup(!!render_node) do |(key, node)|
        name, line_number = key
        next if all_variables.include?(name)
        next if all_global_objects.include?(name)

        node = node.parent
        node = node.parent if %i[condition variable_lookup].include?(node.type_name)

        next if node.variable? && node.filters.any? { |(filter_name)| filter_name == "default" }

        if render_node
          add_offense("Missing argument `#{name}`", node: render_node)
        elsif !info.app_file.partial?
          add_offense("Undefined object `#{name}`", node:, line_number:)
        end
      end
    end
  end
end