# frozen_string_literal: true

module Tooling
  class FeatureFlagDangerfile
    SEE_DOC = "See the [feature flag documentation](https://docs.gitlab.com/ee/development/feature_flags#feature-flag-definition-and-validation)."
    FEATURE_FLAG_LABEL = "feature flag"
    FEATURE_FLAG_EXISTS_LABEL = "#{FEATURE_FLAG_LABEL}::exists".freeze
    FEATURE_FLAG_SKIPPED_LABEL = "#{FEATURE_FLAG_LABEL}::skipped".freeze
    DEVOPS_LABELS_REQUIRING_FEATURE_FLAG_REVIEW = ["devops::verify"].freeze

    SUGGEST_MR_COMMENT = <<~SUGGEST_COMMENT.freeze
    ```suggestion
    group: "%<group>s"
    ```

    #{SEE_DOC}
    SUGGEST_COMMENT

    FEATURE_FLAG_ENFORCEMENT_WARNING = <<~WARNING_MESSAGE.freeze
    There were no new or modified feature flag YAML files detected in this MR.

    If the changes here are already controlled under an existing feature flag, please add
    the ~"#{FEATURE_FLAG_EXISTS_LABEL}". Otherwise, if you think the changes here don't need
    to be under a feature flag, please add the label ~"#{FEATURE_FLAG_SKIPPED_LABEL}", and
    add a short comment about why we skipped the feature flag.

    For guidance on when to use a feature flag, please see the [documentation](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags).
    WARNING_MESSAGE

    def initialize(context:, added_files:, modified_files:, helper:)
      @context = context
      @added_files = added_files
      @modified_files = modified_files
      @helper = helper
    end

    def check_added_feature_flag_files
      added_files.each do |feature_flag|
        check_feature_flag_yaml(feature_flag)
      end
    end

    def check_modified_feature_flag_files
      modified_files.each do |feature_flag|
        check_default_enabled(feature_flag)
      end
    end

    def feature_flag_file_added?
      added_files.any?
    end

    def feature_flag_file_touched?
      touched_feature_flag_files.any?
    end

    def mr_has_backend_or_frontend_changes?
      changes = helper.changes_by_category
      changes.has_key?(:backend) || changes.has_key?(:frontend)
    end

    def stage_requires_feature_flag_review?
      DEVOPS_LABELS_REQUIRING_FEATURE_FLAG_REVIEW.include?(helper.stage_label)
    end

    def mr_missing_feature_flag_status_label?
      ([FEATURE_FLAG_EXISTS_LABEL, FEATURE_FLAG_SKIPPED_LABEL] & helper.mr_labels).none?
    end

    private

    attr_reader :context, :added_files, :modified_files, :helper

    def check_feature_flag_yaml(feature_flag)
      unless feature_flag.valid?
        context.failure("#{helper.html_link(feature_flag.path)} isn't valid YAML! #{SEE_DOC}")
        return
      end

      check_group(feature_flag)
      check_feature_issue_url(feature_flag)
      # Note: we don't check introduced_by_url as it's already done by danger/config_files/Dangerfile
      check_rollout_issue_url(feature_flag)
      check_milestone(feature_flag)
      check_default_enabled(feature_flag)
    end

    def touched_feature_flag_files
      added_files + modified_files
    end

    def check_group(feature_flag)
      mr_group_label = helper.group_label

      if feature_flag.missing_group?
        message_for_feature_flag_missing_group!(feature_flag: feature_flag, mr_group_label: mr_group_label)
      else
        message_for_feature_flag_with_group!(feature_flag: feature_flag, mr_group_label: mr_group_label)
      end
    end

    def message_for_feature_flag_missing_group!(feature_flag:, mr_group_label:)
      if mr_group_label.nil?
        context.failure("Please specify a valid `group` label in #{helper.html_link(feature_flag.path)}. #{SEE_DOC}")
        return
      end

      add_message_on_line(
        feature_flag: feature_flag,
        needle: "group:",
        note: format(SUGGEST_MR_COMMENT, group: mr_group_label),
        fallback_note: %(Please add `group: "#{mr_group_label}"` in #{helper.html_link(feature_flag.path)}. #{SEE_DOC}),
        message_method: :failure
      )
    end

    def message_for_feature_flag_with_group!(feature_flag:, mr_group_label:)
      return if feature_flag.group_match_mr_label?(mr_group_label)

      if mr_group_label.nil?
        helper.labels_to_add << feature_flag.group
      else
        note = <<~FAILURE_MESSAGE
        `group` is set to ~"#{feature_flag.group}" in #{helper.html_link(feature_flag.path)},
        which does not match ~"#{mr_group_label}" set on the MR!
        FAILURE_MESSAGE

        add_message_on_line(
          feature_flag: feature_flag,
          needle: "group:",
          note: note,
          message_method: :failure
        )
      end
    end

    def check_feature_issue_url(feature_flag)
      return unless feature_flag.missing_feature_issue_url?

      add_message_on_line(
        feature_flag: feature_flag,
        needle: "feature_issue_url:",
        note: "Consider filling `feature_issue_url:`"
      )
    end

    def add_message_on_line(feature_flag:, needle:, note:, fallback_note: note, message_method: :message)
      mr_line = feature_flag.find_line_index(needle)

      # rubocop:disable GitlabSecurity/PublicSend -- we allow calling context.message, context.warning & context.failure
      if mr_line
        context.public_send(message_method, note, file: feature_flag.path, line: mr_line.succ)
      else
        context.public_send(message_method, "#{feature_flag.path}: #{fallback_note}")
      end
      # rubocop:enable GitlabSecurity/PublicSend
    end

    def check_rollout_issue_url(feature_flag)
      return unless ::Feature::Shared::TYPES.dig(feature_flag.name.to_sym, :rollout_issue)
      return unless feature_flag.missing_rollout_issue_url?

      missing_field_error(feature_flag: feature_flag, field: :rollout_issue_url)
    end

    def check_milestone(feature_flag)
      return unless feature_flag.missing_milestone?

      missing_field_error(feature_flag: feature_flag, field: :milestone)
    end

    def check_default_enabled(feature_flag)
      return unless feature_flag.default_enabled?

      if ::Feature::Shared.can_be_default_enabled?(feature_flag.type)
        note = <<~SUGGEST_COMMENT
          You're about to [release the feature with the feature flag](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md#optional-release-the-feature-with-the-feature-flag).
          This process can only be done **after** the [global rollout on production](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md#global-rollout-on-production).
          Please make sure in [the rollout issue](#{feature_flag.rollout_issue_url}) that the preliminary steps have already been done. Otherwise, changing the YAML definition might not have the desired effect.
        SUGGEST_COMMENT

        mr_line = feature_flag.find_line_index("default_enabled: true")
        context.markdown(note, file: feature_flag.path, line: mr_line.succ) if mr_line
      else
        context.failure(
          "[Feature flag with the `#{feature_flag.type}` type must not be enabled by default](https://docs.gitlab.com/ee/development/feature_flags/##{feature_flag.type}-type). " \
          "Consider changing the feature flag type if it's ready to be enabled by default."
        )
      end
    end

    def missing_field_error(feature_flag:, field:)
      note = <<~MISSING_FIELD_ERROR
        [Feature flag with the `#{feature_flag.type}` type must have `:#{field}` set](https://docs.gitlab.com/ee/development/feature_flags/##{feature_flag.type}-type).
      MISSING_FIELD_ERROR
      mr_line = feature_flag.find_line_index("#{field}:")

      if mr_line
        context.message(note, file: feature_flag.path, line: mr_line.succ)
      else
        context.message(note)
      end
    end
  end
end

feature_flag_dangerfile = Tooling::FeatureFlagDangerfile.new(
  context: self,
  added_files: feature_flag.feature_flag_files(danger_helper: helper, change_type: :added),
  modified_files: feature_flag.feature_flag_files(danger_helper: helper, change_type: :modified),
  helper: helper
)

feature_flag_dangerfile.check_added_feature_flag_files
feature_flag_dangerfile.check_modified_feature_flag_files

if helper.security_mr? && feature_flag_dangerfile.feature_flag_file_added?
  failure("Feature flags are discouraged from security merge requests. Read the [security documentation](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/utilities/feature_flags.md) for details.")
end

if !helper.security_mr? && feature_flag_dangerfile.mr_has_backend_or_frontend_changes? && feature_flag_dangerfile.stage_requires_feature_flag_review?
  if feature_flag_dangerfile.feature_flag_file_touched? && !helper.mr_has_labels?(Tooling::FeatureFlagDangerfile::FEATURE_FLAG_EXISTS_LABEL)
    # Feature flag config file touched in this MR, so let's add the label to avoid the warning.
    helper.labels_to_add << Tooling::FeatureFlagDangerfile::FEATURE_FLAG_EXISTS_LABEL
  end

  if feature_flag_dangerfile.mr_missing_feature_flag_status_label?
    warn(Tooling::FeatureFlagDangerfile::FEATURE_FLAG_ENFORCEMENT_WARNING)
  end
end
