# frozen_string_literal: true

module Ci
  class PipelineSchedule < Ci::ApplicationRecord
    extend ::Gitlab::Utils::Override
    include Importable
    include StripAttribute
    include CronSchedulable
    include Limitable
    include EachBatch
    include BatchNullifyDependentAssociations
    include Gitlab::Utils::StrongMemoize

    VALID_REF_FORMAT_REGEX = %r{\A(#{Gitlab::Git::TAG_REF_PREFIX}|#{Gitlab::Git::BRANCH_REF_PREFIX})[\S]+}

    SORT_ORDERS = {
      id_asc: { order_by: 'id', sort: 'asc' },
      id_desc: { order_by: 'id', sort: 'desc' },
      description_asc: { order_by: 'description', sort: 'asc' },
      description_desc: { order_by: 'description', sort: 'desc' },
      ref_asc: { order_by: 'ref', sort: 'asc' },
      ref_desc: { order_by: 'ref', sort: 'desc' },
      next_run_at_asc: { order_by: 'next_run_at', sort: 'asc' },
      next_run_at_desc: { order_by: 'next_run_at', sort: 'desc' },
      created_at_asc: { order_by: 'created_at', sort: 'asc' },
      created_at_desc: { order_by: 'created_at', sort: 'desc' },
      updated_at_asc: { order_by: 'updated_at', sort: 'asc' },
      updated_at_desc: { order_by: 'updated_at', sort: 'desc' }
    }.freeze

    self.limit_name = 'ci_pipeline_schedules'
    self.limit_scope = :project

    belongs_to :project
    belongs_to :owner, class_name: 'User'
    has_many :pipelines, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
    has_many :variables, class_name: 'Ci::PipelineScheduleVariable'
    has_many :inputs, class_name: 'Ci::PipelineScheduleInput'

    validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
    validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
    validates :description, presence: true
    validates :variables, nested_attributes_duplicates: true
    validates :inputs, nested_attributes_duplicates: { child_attributes: %i[name] }
    validates :project, presence: true

    validates :inputs, length: {
      maximum: Ci::Pipeline::INPUTS_LIMIT,
      message: ->(*) { _('exceeds the limit of %{count}.') }
    }

    strip_attributes! :cron

    scope :active, -> { where(active: true) }
    scope :inactive, -> { where(active: false) }
    scope :preloaded, -> { preload(:owner, project: [:route]) }
    scope :owned_by, ->(user) { where(owner: user) }
    scope :for_project, ->(project_id) { where(project_id: project_id) }
    scope :with_project_path, -> { includes(project: [:route, { namespace: :route }]) }

    accepts_nested_attributes_for :inputs, allow_destroy: true
    accepts_nested_attributes_for :variables, allow_destroy: true

    alias_attribute :real_next_run, :next_run_at

    def self.sort_by_attribute(method)
      sort_order = SORT_ORDERS[method]
      raise ArgumentError, "order undefined" unless sort_order

      reorder(sort_order[:order_by] => sort_order[:sort])
    end

    def owned_by?(current_user)
      owner == current_user
    end

    def own!(user)
      update(owner: user)
    end

    def inactive?
      !active?
    end

    def deactivate!
      update_attribute(:active, false)
    end

    def job_variables
      variables&.map(&:to_hash_variable) || []
    end

    override :set_next_run_at

    def set_next_run_at
      self.next_run_at = ::Ci::PipelineSchedules::CalculateNextRunService # rubocop: disable CodeReuse/ServiceClass
                           .new(project)
                           .execute(self, fallback_method: method(:calculate_next_run_at))
    end

    # Don't depend on hooks to set the value of `next_run_at` as
    # allow_next_run_at_update? will interfere with the process.
    # Explicitly set the value here before each save operation instead.
    override :schedule_next_run!
    def schedule_next_run!
      if Feature.enabled?(:resolve_pipeline_schedule_race_conditions, project)
        return if cron_values_changed?

        set_next_run_at
      end

      super
    end

    def daily_limit
      project.actual_limits.limit_for(:ci_daily_pipeline_schedule_triggers)
    end

    def ref_for_display
      return unless ref.present?

      ref.gsub(%r{^refs/(heads|tags)/}, '')
    end

    def for_tag?
      return false unless ref.present?

      ref.start_with? 'refs/tags/'
    end

    def worker_cron_expression
      Settings.cron_jobs['pipeline_schedule_worker']['cron']
    end

    # Using destroy instead of before_destroy as we want nullify_dependent_associations_in_batches
    # to run first and not in a transaction block. This prevents timeouts for schedules with numerous pipelines
    def destroy
      nullify_dependent_associations_in_batches

      super
    end

    def expand_short_ref
      return if ref.blank? || VALID_REF_FORMAT_REGEX.match?(ref) || ambiguous_ref?

      # In case the ref doesn't exist default to the initial value
      self.ref = project.repository.expand_ref(ref) || ref
    end

    def inputs_hash
      inputs.to_h { |input| [input.name, input.value] }
    end

    def last_pipeline
      pipelines.last
    end

    private

    def ambiguous_ref?
      strong_memoize_with(:ambiguous_ref, ref) do
        project.repository.ambiguous_ref?(ref)
      end
    end

    def cron_values_changed?
      cron_changed? || cron_timezone_changed?
    end

    # This method will block updates to next_run_at on simple record mutations
    # that don't affect the cron or the cron_timezone.
    # Operations that explicitly set the next_run_at should
    # call schedule_next_run!
    override :allow_next_run_at_update?
    def allow_next_run_at_update?
      return true unless Feature.enabled?(:resolve_pipeline_schedule_race_conditions, project)

      cron_values_changed?
    end
  end
end

Ci::PipelineSchedule.prepend_mod_with('Ci::PipelineSchedule')
