# frozen_string_literal: true

module Ci
  class BuildDependencies
    include ::Gitlab::Utils::StrongMemoize

    attr_reader :processable

    def initialize(processable)
      @processable = processable
    end

    def all
      (local + cross_pipeline + cross_project).uniq
    end

    def invalid
      [local, cross_pipeline, cross_project].flat_map { |deps| deps.reject(&:valid_dependency?) }
    end

    def valid?
      valid_local? && valid_cross_pipeline? && valid_cross_project?
    end

    private

    # Dependencies can only be of Ci::Build type because only builds
    # can create artifacts
    def model_class
      ::Ci::Build
    end

    # Dependencies local to the given pipeline
    def local
      strong_memoize(:local) do
        next [] if no_local_dependencies_specified?
        next [] unless processable.pipeline_id # we don't have any dependency when creating the pipeline

        deps = model_class.where(pipeline_id: processable.pipeline_id, partition_id: processable.partition_id).latest
        deps = find_dependencies(processable, deps)

        from_dependencies(deps).to_a
      end
    end

    def find_dependencies(processable, deps)
      if processable.scheduling_type_dag?
        from_needs(deps)
      else
        from_previous_stages(deps)
      end
    end

    # Dependencies from the same parent-pipeline hierarchy excluding
    # the current job's pipeline
    def cross_pipeline
      strong_memoize(:cross_pipeline) do
        fetch_dependencies_in_hierarchy
      end
    end

    # Dependencies that are defined by project and ref
    def cross_project
      []
    end

    def fetch_dependencies_in_hierarchy
      deps_specifications = specified_cross_pipeline_dependencies
      return [] if deps_specifications.empty?

      deps_specifications = expand_variables_and_validate(deps_specifications)
      jobs_in_pipeline_hierarchy(deps_specifications)
    end

    def jobs_in_pipeline_hierarchy(deps_specifications)
      all_pipeline_ids = []
      all_job_names = []

      deps_specifications.each do |spec|
        all_pipeline_ids << spec[:pipeline]
        all_job_names << spec[:job]
      end

      model_class.latest.success
        .in_pipelines(processable.pipeline.same_family_pipeline_ids)
        .in_pipelines(all_pipeline_ids.uniq)
        .by_name(all_job_names.uniq)
        .select do |dependency|
          # the query may not return exact matches pipeline-job, so we filter
          # them separately.
          deps_specifications.find do |spec|
            spec[:pipeline] == dependency.pipeline_id &&
              spec[:job] == dependency.name
          end
        end
    end

    def expand_variables_and_validate(specifications)
      specifications.filter_map do |spec|
        pipeline = ExpandVariables.expand(spec[:pipeline].to_s, processable_variables).to_i
        # current pipeline is not allowed because local dependencies
        # should be used instead.
        next if pipeline == processable.pipeline_id

        job = ExpandVariables.expand(spec[:job], processable_variables)

        { job: job, pipeline: pipeline }
      end
    end

    def valid_cross_pipeline?
      cross_pipeline.size == specified_cross_pipeline_dependencies.size
    end

    def valid_local?
      local.all?(&:valid_dependency?)
    end

    def valid_cross_project?
      true
    end

    def project
      processable.project
    end

    def no_local_dependencies_specified?
      processable.options[:dependencies]&.empty?
    end

    def from_previous_stages(scope)
      scope.before_stage(processable.stage_idx)
    end

    def from_needs(scope)
      needs_names = processable.needs.artifacts.select(:name)
      scope.where(name: needs_names)
    end

    def from_dependencies(scope)
      return scope unless processable.options[:dependencies].present?

      scope.where(name: processable.options[:dependencies])
    end

    def processable_variables
      -> { processable.simple_variables_without_dependencies }
    end

    def specified_cross_pipeline_dependencies
      strong_memoize(:specified_cross_pipeline_dependencies) do
        specified_cross_dependencies.select { |dep| dep[:pipeline] && dep[:artifacts] }
      end
    end

    def specified_cross_dependencies
      Array(processable.options[:cross_dependencies])
    end
  end
end

Ci::BuildDependencies.prepend_mod_with('Ci::BuildDependencies')
