# frozen_string_literal: true

module OpsController::Settings::LabelTagMapping
  extend ActiveSupport::Concern

  # TODO: Multi-provider support is hacky:
  # --------------------------------------
  # Currently we store in labeled_resource_type an arbitrary string that
  # sometimes looks like a model name, sometimes fake string like "Vm" -
  # all that matters is these strings match what refresh passes to `map_labels`.
  #
  # In any case, this requires different providers use disjoint sets of strings.
  MappableEntity = Struct.new(:prefix, :model)

  ALL_ENTITIES = "_all_entities_"

  MAPPABLE_ENTITIES = {
    # TODO: support per-provider "All Amazon" etc?
    # Currently we have only global "All".
    # Global "All" categories are named "kubernetes::..." for backward compatibility.
    nil                   => MappableEntity.new("kubernetes::",
                                                nil),
    ALL_ENTITIES          => MappableEntity.new(nil,
                                                "All Entities"),
    "VmAmazon"            => MappableEntity.new("amazon:vm:",
                                                "ManageIQ::Providers::Amazon::CloudManager::Vm"),
    "VmIBM"               => MappableEntity.new("ibm:vm:",
                                                "ManageIQ::Providers::IbmCloud::VPC::CloudManager::Vm"),
    "VmOpenstack"         => MappableEntity.new("openstack:vm:",
                                                "ManageIQ::Providers::Openstack::CloudManager::Vm"),
    "VmAzure"             => MappableEntity.new("azure:vm:",
                                                "ManageIQ::Providers::Azure::CloudManager::Vm"),
    "VmVmware"            => MappableEntity.new("vmware:vm:",
                                                "ManageIQ::Providers::Vmware::InfraManager::Vm"),
    "ImageAmazon"         => MappableEntity.new("amazon:image:",
                                                "ManageIQ::Providers::Amazon::CloudManager::Template"),
    "ContainerProject"    => MappableEntity.new("kubernetes:container_project:",
                                                "ContainerProject"),
    "ContainerRoute"      => MappableEntity.new("kubernetes:container_route:",
                                                "ContainerRoute"),
    "ContainerNode"       => MappableEntity.new("kubernetes:container_node:",
                                                "ContainerNode"),
    "ContainerReplicator" => MappableEntity.new("kubernetes:container_replicator:",
                                                "ContainerReplicator"),
    "ContainerService"    => MappableEntity.new("kubernetes:container_service:",
                                                "ContainerService"),
    "ContainerGroup"      => MappableEntity.new("kubernetes:container_group:",
                                                "ContainerGroup"),
    "ContainerBuild"      => MappableEntity.new("kubernetes:container_build:",
                                                "ContainerBuild"),
  }.freeze

  def label_tag_mapping_edit
    assert_privileges("region_edit")

    case params[:button]
    when "cancel"
      @lt_map = session[:edit][:lt_map] if session[:edit] && session[:edit][:lt_map]
      if !@lt_map || @lt_map.id.blank?
        add_flash(_("Add of new Provider Tag Mapping was cancelled by the user"))
      else
        add_flash(_("Edit of Provider Tag Mapping \"%{name}\" was cancelled by the user") % {:name => @lt_map.label_name})
      end
      get_node_info(x_node)
      @lt_map = @edit = session[:edit] = nil # clean out the saved info
      render :update do |page|
        page << javascript_prologue
        page.replace("flash_msg_div", :partial => "layouts/flash_msg")
        page << "miqScrollTop();" if @flash_array.present?
        page.replace_html('map_tags', :partial => 'settings_map_tags_tab')
      end
    when "save", "add"
      id = params[:id] || "new"
      return unless load_edit("label_tag_mapping_edit__#{id}", "replace_cell__explorer")

      @lt_map = @edit[:lt_map] if @edit && @edit[:lt_map]
      if @edit[:new][:entity].blank?
        add_flash(_("Entity is required"), :error)
      end
      if @edit[:new][:label_name].blank?
        add_flash(_("Label is required"), :error)
      end
      if @edit[:new][:category].blank?
        add_flash(_("Category is required"), :error)
      end
      unless @flash_array.nil?
        javascript_flash
        return
      end
      if params[:button] == "add"
        label_tag_mapping_add(@edit[:new][:entity], @edit[:new][:label_name], @edit[:new][:category])
      else # save
        label_tag_mapping_update(@lt_map.id, @edit[:new][:category])
      end
    when "reset", nil # Reset or first time in
      if params[:id]
        @lt_map = ProviderTagMapping.find(params[:id])
        lt_map_set_form_vars
      else
        lt_map_set_new_form_vars
      end
      @in_a_form = true
      session[:changed] = false
      if params[:button] == "reset"
        add_flash(_("All changes have been reset"), :warning)
      end
      replace_right_cell(:nodetype => "ltme")
    end
  end

  def entity_ui_name_or_all(entity)
    if entity
      if entity == ALL_ENTITIES
        _(MAPPABLE_ENTITIES[entity].model)
      else
        model = MAPPABLE_ENTITIES[entity].model
        ui_lookup(:model => model)
      end
    else
      _("<Choose>")
    end
  end

  def entity_options
    MAPPABLE_ENTITIES.collect do |entity, _provider|
      [entity_ui_name_or_all(entity), entity]
    end
  end

  def label_tag_mapping_get_all
    # Current UI only supports any-value -> category mappings
    mappings = ProviderTagMapping.in_my_region.where(:label_value => nil)

    # This renders into label_tag_mapping_form view, fields are different from other
    # functions here, notably `:entity` is the translated ui name.
    @lt_mapping = []
    @any_mapping_is_all_entities = false
    mappings.each do |m|
      lt_map = {}
      lt_map[:id] = m.id
      lt_map[:entity] = entity_ui_name_or_all(m.labeled_resource_type)
      lt_map[:label_name] = m.label_name
      tag_classification = m.tag.classification
      lt_map[:category] = cat_description_without_prefix(tag_classification.description)
      @any_mapping_is_all_entities = true if m.labeled_resource_type == ALL_ENTITIES
      @lt_mapping.push(lt_map)
    end
  end

  # Set form variables for mapping edit (initially or after reset)
  def lt_map_set_form_vars
    @edit = {}
    @edit[:lt_map] = @lt_map
    @edit[:new] = {}
    @edit[:key] = "label_tag_mapping_edit__#{@lt_map.id || "new"}"
    @edit[:new][:entity] = @lt_map.labeled_resource_type
    @edit[:new][:label_name] = @lt_map.label_name
    @edit[:new][:category] = cat_description_without_prefix(@lt_map.tag.classification.description)
    @edit[:current] = copy_hash(@edit[:new])
    @edit[:new][:options] = entity_options
    session[:edit] = @edit
  end

  # Set form variables for mapping add
  def lt_map_set_new_form_vars
    @edit = {}
    # no :lt_map
    @edit[:new] = {}
    @edit[:key] = "label_tag_mapping_edit__new"
    @edit[:new][:entity] = nil
    @edit[:new][:label_name] = nil
    @edit[:new][:category] = nil
    @edit[:current] = copy_hash(@edit[:new])
    @edit[:new][:options] = entity_options
    session[:edit] = @edit
  end

  def categories_for_select
    Classification.categories.reject(&:read_only?).sort_by(&:description).map(&:description)
  end

  # AJAX driven routine to check for changes in ANY field on the user form
  def label_tag_mapping_field_changed
    assert_privileges("region_edit")

    return unless load_edit("label_tag_mapping_edit__#{params[:id]}", "replace_cell__explorer")

    lt_map_get_form_vars
    @changed = (@edit[:new] != @edit[:current])

    controller_object = self

    render :update do |page|
      page << javascript_prologue

      if params[:entity]
        page.replace("tag_category_div", :partial => "ops/label_tag_mapping/tag_category",
                                         :locals  => controller_object.tag_category_parameters_for_haml)
      end

      if @refresh_div
        page.replace(@refresh_div,
                     :partial => @refresh_partial,
                     :locals  => {:type       => "provider_tag_mapping",
                                  :action_url => 'label_tag_mapping_field_changed'})
      end
      page << javascript_for_miq_button_visibility_changed(@changed)
    end
  end

  # Get variables from label_tag_mapping edit form
  def lt_map_get_form_vars
    @lt_map = @edit[:lt_map]
    copy_params_if_present(@edit[:new], params, %i[entity label_name category])
  end

  def flash_message_on_validation_error_for(validation_error, entity, label_name, cat_description)
    entity_name = entity_ui_name_or_all(entity)
    variables = {:entity => entity_name, :label => label_name, :cat_description => cat_description}
    message = if validation_error == :unique_mapping
                _("Mapping for \"%{entity}\", Label \"%{label}\" and Tag Category \"%{cat_description}\" already exists") % variables
              else
                _("Mapping for \"%{entity}\", Label \"%{label}\": Tag Category \"%{cat_description}\" must exist") % variables
              end

    add_flash(message, :error)
    javascript_flash
  end

  def classification_lookup_with_cache_by(cat_description)
    @classification ||= {}
    @classification[cat_description] ||= Classification.lookup_category_by_description(cat_description)
  end

  def category_for_mapping(cat_description, entity)
    if entity == ALL_ENTITIES
      classification_lookup_with_cache_by(cat_description)
    else
      Classification.create_category!(:name         => category_name_from_description(entity, cat_description),
                                      :description  => cat_description_with_prefix(entity, cat_description),
                                      :single_value => true,
                                      :read_only    => true)
    end
  end

  def category_name_from_description(entity, cat_description)
    cat_prefix = MAPPABLE_ENTITIES[entity].prefix
    cat_prefix.to_s + Classification.sanitize_name(cat_description.tr("/", ":"))
  end

  def find_prefixed_category_for_mapping_by(cat_description, entity)
    # UI currently can't allow 2 mappings for same (entity, label).
    category = Classification.is_category.read_only.find_by(:single_value => true, :description => cat_description)
    category || Classification.lookup_by_name(category_name_from_description(entity, cat_description))
  end

  def validate_mapping(cat_description, entity, label_name)
    tag = classification_lookup_with_cache_by(cat_description)&.tag
    return :unique_mapping if tag && ProviderTagMapping.where(:label_name => label_name, :tag => tag).exists?

    if entity == ALL_ENTITIES
      :tag_not_found unless classification_lookup_with_cache_by(cat_description)
    elsif find_prefixed_category_for_mapping_by(cat_description, entity)
      :unique_mapping
    end
  end

  def label_tag_mapping_add(entity, label_name, cat_description)
    if (error = validate_mapping(cat_description, entity, label_name))
      flash_message_on_validation_error_for(error, entity, label_name, cat_description)
      return
    end

    begin
      ActiveRecord::Base.transaction do
        category = category_for_mapping(cat_description, entity)
        ProviderTagMapping.create!(:labeled_resource_type => entity,
                                   :label_name            => label_name,
                                   :tag                   => category.tag)
      end
    rescue StandardError => bang
      add_flash(_("Error during 'add': %{message}") % {:message => bang.message}, :error)
      javascript_flash
    else
      add_flash(_("Provider Tag Mapping \"%{name}\" was added") % {:name => label_name})
      get_node_info(x_node)
      @lt_map = @edit = session[:edit] = nil # clean out the saved info
      render :update do |page|
        page << javascript_prologue
        page.replace("flash_msg_div", :partial => "layouts/flash_msg")
        page << "miqScrollTop();" if @flash_array.present?
        page.replace_html('map_tags', :partial => 'settings_map_tags_tab')
      end
    end
  end

  def label_tag_mapping_update(id, cat_description)
    assert_privileges("region_edit")
    mapping = ProviderTagMapping.find(id)
    begin
      if mapping.labeled_resource_type == ALL_ENTITIES
        update_category = classification_lookup_with_cache_by(cat_description)
        # Should not create a new category if "All entities". The chosen category should exist
        if update_category.nil?
          flash_message_on_validation_error_for(:tag_not_found, mapping.labeled_resource_type, mapping.label_name, cat_description)
          return
        end
        mapping.tag = update_category.tag
        mapping.save!
      else
        update_category             = mapping.tag.classification
        update_category.name        = category_name_from_description(mapping.labeled_resource_type, cat_description)
        update_category.description = cat_description_with_prefix(mapping.labeled_resource_type, cat_description)
        update_category.save!
      end
    rescue StandardError => bang
      add_flash(_("Error during 'save': %{message}") % {:message => bang.message}, :error)
      javascript_flash
    else
      add_flash(_("Provider Tag Mapping \"%{name}\" was saved") % {:name => mapping.label_name})
      get_node_info(x_node)
      @lt_map = @edit = session[:edit] = nil # clean out the saved info
      render :update do |page|
        page << javascript_prologue
        page.replace("flash_msg_div", :partial => "layouts/flash_msg")
        page << "miqScrollTop();" if @flash_array.present?
        page.replace_html('map_tags', :partial => 'settings_map_tags_tab')
      end
    end
  end

  def label_tag_mapping_delete
    assert_privileges("region_edit")
    mapping = ProviderTagMapping.find(params[:id])
    category = mapping.tag.classification
    label_name = mapping.label_name

    deleted = false
    ActiveRecord::Base.transaction do
      deleted = if mapping.labeled_resource_type == ALL_ENTITIES
                  # Don't delete the category in this case because it was created outside of tag mapping
                  # Note: As a side effect, taggings of this category will not be removed from resources that had this mapping
                  mapping.destroy
                else
                  # delete mapping and category - will indirectly delete tags
                  mapping.destroy && category.destroy
                end
    end

    if deleted
      add_flash(_("Provider Tag Mapping \"%{name}\": Delete successful") % {:name => label_name})
      label_tag_mapping_get_all
      render :update do |page|
        page << javascript_prologue
        page.replace("flash_msg_div", :partial => "layouts/flash_msg")
        page << "miqScrollTop();" if @flash_array.present?
        page.replace_html('map_tags', :partial => 'settings_map_tags_tab')
      end
    else
      mapping.errors.each { |error| add_flash("#{error.attribute.to_s.capitalize} #{error.message}", :error) }
      category.errors.each { |error| add_flash("#{error.attribute.to_s.capitalize} #{error.message}", :error) }
      javascript_flash
    end
  end

  def cat_description_with_prefix(entity, cat_description)
    prefix = MAPPABLE_ENTITIES[entity].prefix.chomp(":")
    "#{prefix}|#{cat_description}"
  end

  def cat_description_without_prefix(cat_description)
    cat_description.sub(/^[^\|]+\|/, "")
  end

  included do
    helper_method :tag_category_parameters_for_haml

    def tag_category_parameters_for_haml
      {:lt_map       => @lt_map,
       :categories   => categories_for_select,
       :category     => @edit[:new][:category],
       :all_entities => @edit[:new][:entity] == ALL_ENTITIES}
    end
  end
end
