# frozen_string_literal: true

module Gitlab
  module Ci
    class Lint
      class Result
        attr_reader :jobs, :merged_yaml, :errors, :warnings, :includes

        def initialize(jobs:, merged_yaml:, errors:, warnings:, includes:)
          @jobs = jobs
          @merged_yaml = merged_yaml
          @errors = errors
          @warnings = warnings
          @includes = includes
        end

        def valid?
          @errors.empty?
        end

        def status
          valid? ? :valid : :invalid
        end
      end

      LOG_MAX_DURATION_THRESHOLD = 2.seconds

      def initialize(project:, current_user:, sha: nil, verify_project_sha: true)
        @project = project
        @current_user = current_user

        #
        # We are deprecating these parameters because we'll replace the `legacy_validate` with the `validate` method.
        #
        # If the `sha` is not provided, the default is the project's head commit (or nil). In such case, we
        # don't need to call `YamlProcessor.verify_project_sha!`, which prevents redundant calls to Gitaly.
        @legacy_verify_project_sha = verify_project_sha && sha.present?
        @legacy_sha = sha || project&.repository&.commit&.sha
      end

      # Our goal is to remove the `sha` dependency and the custom `YamlProcessor` usage from the CI linting logic.
      # This legacy method is aimed to be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/543727.
      def legacy_validate(content, dry_run: false, ref: project&.default_branch)
        if dry_run
          simulate_pipeline_creation(content, ref)
        else
          legacy_static_validation(content)
        end
      end

      def validate(content, dry_run:, ref:)
        if dry_run
          simulate_pipeline_creation(content, ref)
        else
          lint_pipeline_creation(content, ref)
        end
      end

      private

      attr_accessor :project, :current_user, :legacy_sha, :legacy_verify_project_sha

      def simulate_pipeline_creation(content, ref)
        pipeline = ::Ci::CreatePipelineService
          .new(@project, @current_user, ref: ref)
          .execute(:push, dry_run: true, content: content)
          .payload

        Result.new(
          jobs: dry_run_convert_to_jobs(pipeline.stages),
          merged_yaml: pipeline.config_metadata.try(:[], :merged_yaml),
          errors: pipeline.error_messages.map(&:content),
          warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content),
          includes: pipeline.config_metadata.try(:[], :includes)
        )
      end

      def lint_pipeline_creation(content, ref)
        service = ::Ci::CreatePipelineService.new(@project, @current_user, ref: ref)
        pipeline = service.execute(:push, linting: true, content: content).payload

        Result.new(
          jobs: yaml_processor_result_to_jobs(service.yaml_processor_result),
          merged_yaml: pipeline.config_metadata.try(:[], :merged_yaml),
          errors: pipeline.error_messages.map(&:content),
          warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content),
          includes: pipeline.config_metadata.try(:[], :includes)
        )
      end

      def legacy_static_validation(content)
        logger = build_logger

        result = yaml_processor_result(content, logger)

        Result.new(
          jobs: yaml_processor_result_to_jobs(result),
          merged_yaml: result.config_metadata[:merged_yaml],
          errors: result.errors,
          warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT), # rubocop: disable CodeReuse/ActiveRecord
          includes: result.config_metadata[:includes]
        )
      ensure
        logger.commit(pipeline: ::Ci::Pipeline.new, caller: self.class.name)
      end

      def yaml_processor_result(content, logger)
        logger.instrument(:yaml_process, once: true) do
          Gitlab::Ci::YamlProcessor.new(content, project: project,
            user: current_user,
            ref: find_ref,
            sha: legacy_sha,
            verify_project_sha: legacy_verify_project_sha,
            logger: logger).execute
        end
      end

      def dry_run_convert_to_jobs(stages)
        stages.reduce([]) do |jobs, stage|
          jobs + stage.statuses.map do |job|
            {
              name: job.name,
              stage: stage.name,
              before_script: job.options[:before_script].to_a,
              script: job.options[:script].to_a,
              after_script: job.options[:after_script].to_a,
              tag_list: (job.tag_list if job.is_a?(::Ci::Build)).to_a,
              environment: job.options.dig(:environment, :name),
              when: job.when,
              allow_failure: job.allow_failure
            }
          end
        end
      end

      def yaml_processor_result_to_jobs(result)
        jobs = []
        return jobs unless result&.valid?

        result.stages.each do |stage_name|
          result.builds.each do |job|
            next unless job[:stage] == stage_name

            jobs << {
              name: job[:name],
              stage: stage_name,
              before_script: job.dig(:options, :before_script).to_a,
              script: job.dig(:options, :script).to_a,
              after_script: job.dig(:options, :after_script).to_a,
              tag_list: job[:tag_list].to_a,
              only: job[:only],
              except: job[:except],
              environment: job[:environment],
              when: job[:when],
              allow_failure: job[:allow_failure],
              needs: job[:needs_attributes]
            }
          end
        end

        jobs
      end

      def build_logger
        Gitlab::Ci::Pipeline::Logger.new(project: project) do |l|
          l.log_when do |observations|
            duration = observations['yaml_process_duration_s']
            next false unless duration

            duration >= LOG_MAX_DURATION_THRESHOLD
          end
        end
      end

      def find_ref
        ref = RefFinder.new(project).find_by_sha(legacy_sha)

        return unless ref

        allowed_to_write_ref?(ref) ? ref : nil
      end

      def allowed_to_write_ref?(ref)
        access = Gitlab::UserAccess.new(current_user, container: project)

        # We only check access for protected branches and not for protected tags
        # because it's not possible to perform static validation on a protected tag
        # as we initialize a new `Ci::Pipeline` in `Ci::Config` and do not pass `tag: true`.
        access.can_update_branch?(ref)
      end
    end
  end
end
