module Gitlab
  module Ci
    class YamlProcessor
      ValidationError = Class.new(StandardError)

      include Gitlab::Ci::Config::Entry::LegacyValidationHelpers

      attr_reader :path, :cache, :stages, :jobs

      def initialize(config, path = nil)
        @ci_config = Gitlab::Ci::Config.new(config)
        @config = @ci_config.to_hash
        @path = path

        unless @ci_config.valid?
          raise ValidationError, @ci_config.errors.first
        end

        initial_parsing
      rescue Gitlab::Ci::Config::Loader::FormatError => e
        raise ValidationError, e.message
      end

      def builds
        @jobs.map do |name, _|
          build_attributes(name)
        end
      end

      def build_attributes(name)
        job = @jobs[name.to_sym] || {}

        { stage_idx: @stages.index(job[:stage]),
          stage: job[:stage],
          commands: job[:commands],
          tag_list: job[:tags] || [],
          name: job[:name].to_s,
          allow_failure: job[:ignore],
          when: job[:when] || 'on_success',
          environment: job[:environment_name],
          coverage_regex: job[:coverage],
          yaml_variables: yaml_variables(name),
          options: {
            image: job[:image],
            services: job[:services],
            artifacts: job[:artifacts],
            cache: job[:cache],
            dependencies: job[:dependencies],
            before_script: job[:before_script],
            script: job[:script],
            after_script: job[:after_script],
            environment: job[:environment],
            retry: job[:retry]
          }.compact }
      end

      def pipeline_stage_builds(stage, pipeline)
        selected_jobs = @jobs.select do |_, job|
          next unless job[:stage] == stage

          only_specs = Gitlab::Ci::Build::Policy
            .fabricate(job.fetch(:only, {}))
          except_specs = Gitlab::Ci::Build::Policy
            .fabricate(job.fetch(:except, {}))

          only_specs.all? { |spec| spec.satisfied_by?(pipeline) } &&
            except_specs.none? { |spec| spec.satisfied_by?(pipeline) }
        end

        selected_jobs.map { |_, job| build_attributes(job[:name]) }
      end

      def stage_seeds(pipeline)
        seeds = @stages.uniq.map do |stage|
          builds = pipeline_stage_builds(stage, pipeline)

          Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
        end

        seeds.compact
      end

      def self.validation_message(content)
        return 'Please provide content of .gitlab-ci.yml' if content.blank?

        begin
          Gitlab::Ci::YamlProcessor.new(content)
          nil
        rescue ValidationError, Psych::SyntaxError => e
          e.message
        end
      end

      private

      def initial_parsing
        ##
        # Global config
        #
        @before_script = @ci_config.before_script
        @image = @ci_config.image
        @after_script = @ci_config.after_script
        @services = @ci_config.services
        @variables = @ci_config.variables
        @stages = @ci_config.stages
        @cache = @ci_config.cache

        ##
        # Jobs
        #
        @jobs = @ci_config.jobs

        @jobs.each do |name, job|
          # logical validation for job

          validate_job_stage!(name, job)
          validate_job_dependencies!(name, job)
          validate_job_environment!(name, job)
        end
      end

      def yaml_variables(name)
        variables = (@variables || {})
          .merge(job_variables(name))

        variables.map do |key, value|
          { key: key.to_s, value: value, public: true }
        end
      end

      def job_variables(name)
        job = @jobs[name.to_sym]
        return {} unless job

        job[:variables] || {}
      end

      def validate_job_stage!(name, job)
        return unless job[:stage]

        unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
          raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
        end
      end

      def validate_job_dependencies!(name, job)
        return unless job[:dependencies]

        stage_index = @stages.index(job[:stage])

        job[:dependencies].each do |dependency|
          raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]

          unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
            raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
          end
        end
      end

      def validate_job_environment!(name, job)
        return unless job[:environment]
        return unless job[:environment].is_a?(Hash)

        environment = job[:environment]
        validate_on_stop_job!(name, environment, environment[:on_stop])
      end

      def validate_on_stop_job!(name, environment, on_stop)
        return unless on_stop

        on_stop_job = @jobs[on_stop.to_sym]
        unless on_stop_job
          raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
        end

        unless on_stop_job[:environment]
          raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
        end

        unless on_stop_job[:environment][:name] == environment[:name]
          raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
        end

        unless on_stop_job[:environment][:action] == 'stop'
          raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
        end
      end
    end
  end
end