module Katello
  class Api::V2::ContentViewsController < Api::V2::ApiController
    include Concerns::Authorization::Api::V2::ContentViewsController
    include Katello::Concerns::FilteredAutoCompleteSearch
    include Katello::Concerns::Api::V2::BulkExtensions

    before_action :find_authorized_katello_resource, :except => [:index, :create, :copy, :auto_complete_search]
    before_action :ensure_non_generated, only: [:publish]
    before_action :ensure_non_default, :except => [:index, :create, :copy, :auto_complete_search]
    before_action :find_organization, :only => [:create]
    before_action :find_optional_organization, :only => [:index, :auto_complete_search]
    before_action :find_environment, :only => [:index, :remove_from_environment]

    wrap_parameters :include => (ContentView.attribute_names + %w(repository_ids component_ids))

    resource_description do
      api_version "v2"
    end

    def_param_group :content_view do
      param :description, String, :desc => N_("Description for the content view")
      param :repository_ids, Array, :desc => N_("List of repository ids")
      param :component_ids, Array, :desc => N_("List of component content view version ids for composite views")
      param :environment_ids, Array, :desc => N_("List of lifecycle environments for rolling content views")
      param :auto_publish, :bool, :desc => N_("Enable/Disable auto publish of composite view")
      param :solve_dependencies, :bool, :desc => N_("Solve RPM dependencies by default on Content View publish, defaults to false")
      param :import_only, :bool, :desc => N_("Designate this Content View for importing from upstream servers only. Defaults to false")
    end

    def_param_group :bulk_content_view_version_ids do
      param :included, Hash, :desc => N_("Versions to exclusively include in the action"), :required => true, :action_aware => true do
        param :search, String, :required => false, :desc => N_("Search string for versions to perform an action on")
        param :ids, Array, :required => false, :desc => N_("List of versions to perform an action on")
      end
      param :excluded, Hash, :desc => N_("Versions to explicitly exclude in the action."\
                                         " All other versions will be included in the action,"\
                                         " unless an included parameter is passed as well."), :required => true, :action_aware => true do
        param :ids, Array, :required => false, :desc => N_("List of versions to exclude and not run an action on")
      end
    end

    def filtered_associations
      {
        :component_ids => Katello::ContentViewVersion,
        :repository_ids => Katello::Repository,
        :environment_ids => Katello::KTEnvironment,
      }
    end

    api :GET, "/organizations/:organization_id/content_views", N_("List content views")
    api :GET, "/content_views", N_("List content views")
    param :organization_id, :number, :desc => N_("organization identifier")
    param :environment_id, :number, :desc => N_("environment identifier")
    param :nondefault, :bool, :desc => N_("Filter out default content views")
    param :noncomposite, :bool, :desc => N_("Filter out composite content views")
    param :composite, :bool, :desc => N_("Filter only composite content views")
    param :without, Array, :desc => N_("Do not include this array of content views")
    param :name, String, :desc => N_("Name of the content view"), :required => false
    param :label, String, :desc => N_("Label of the content view"), :required => false
    param :include_generated, :bool, :desc => N_("Include content views generated by imports/exports. Defaults to false")
    param_group :search, Api::V2::ApiController
    add_scoped_search_description_for(ContentView)
    def index
      content_view_includes = [:activation_keys, :content_view_versions, :content_view_components,
                               :environments, :organization, :repositories]
      respond(:collection => scoped_search(index_relation.distinct, :name, :asc, :includes => content_view_includes))
    end

    def index_relation
      content_views = ContentView.readable
      content_views = content_views.ignore_generated unless Foreman::Cast.to_bool(params[:include_generated])
      content_views = content_views.where(:organization_id => @organization.id) if @organization
      content_views = content_views.in_environment(@environment) if @environment
      content_views = ::Foreman::Cast.to_bool(params[:nondefault]) ? content_views.non_default : content_views.default if params[:nondefault]
      content_views = ::Foreman::Cast.to_bool(params[:noncomposite]) ? content_views.non_composite : content_views.composite if params[:noncomposite]
      content_views = ::Foreman::Cast.to_bool(params[:composite]) ? content_views.composite : content_views.non_composite if params[:composite]
      content_views = content_views.where(:name => params[:name]) if params[:name]
      content_views = content_views.where(:label => params[:label]) if params[:label]
      content_views = content_views.where("#{ContentView.table_name}.id NOT IN (?)", params[:without]) if params[:without]
      content_views
    end

    api :POST, "/organizations/:organization_id/content_views", N_("Create a content view")
    api :POST, "/content_views", N_("Create a content view")
    param :organization_id, :number, :desc => N_("Organization identifier"), :required => true
    param :name, String, :desc => N_("Name of the content view"), :required => true
    param :label, String, :desc => N_("Content view label")
    param :composite, :bool, :desc => N_("Composite content view")
    param :rolling, :bool, :desc => N_("Rolling content view")
    param_group :content_view
    def create
      validate_environment_ids!(params[:rolling])
      @content_view = ContentView.create!(view_params) do |view|
        view.organization = @organization
        view.label ||= labelize_params(params[:content_view])
      end
      sync_task(::Actions::Katello::ContentView::Create, @content_view, sanitized_environment_ids)

      respond :resource => @content_view
    end

    api :PUT, "/content_views/:id", N_("Update a content view")
    param :id, :number, :desc => N_("Content view identifier"), :required => true
    param :name, String, :desc => N_("New name for the content view")
    param_group :content_view
    def update
      validate_environment_ids!(@content_view.rolling?)
      sync_task(::Actions::Katello::ContentView::Update, @content_view, view_params, sanitized_environment_ids)
      respond :resource => @content_view.reload
    end

    api :POST, "/content_views/:id/publish", N_("Publish a content view")
    param :id, :number, :desc => N_("Content view identifier"), :required => true
    param :description, String, :desc => N_("Description for the new published content view version")
    param :major, :number, :desc => N_("Override the major version number"), :required => false
    param :minor, :number, :desc => N_("Override the minor version number"), :required => false
    param :environment_ids, Array, :desc => N_("Identifiers for Lifecycle Environment"), :required => false
    param :publish_only_if_needed, :bool, :desc => N_("Check audited changes and proceed only if content or filters have changed since last publish"), :required => false
    param :is_force_promote, :bool, :desc => N_("Force content view promotion and bypass lifecycle environment restriction"), :required => false
    param :repos_units, Array, :desc => N_("Specify the list of units in each repo"), :required => false do
      param :label, String, :desc => N_("repo label"), :required => true
      param :rpm_filenames, Array, of: String, :desc => N_("list of rpm filename strings to include in published version"), :required => true
    end
    def publish
      validate_publish_params!

      task = async_task(::Actions::Katello::ContentView::Publish, @content_view, params[:description],
                        :environment_ids => params[:environment_ids],
                        :is_force_promote => params[:is_force_promote],
                        :major => params[:major],
                        :minor => params[:minor],
                        :repos_units => params[:repos_units])
      respond_for_async :resource => task
    end

    api :GET, "/content_views/:id", N_("Show a content view")
    param :id, :number, :desc => N_("content view numeric identifier"), :required => true
    def show
      respond :resource => @content_view
    end

    api :DELETE, "/content_views/:id/environments/:environment_id", N_("Remove a content view from an environment")
    param :id, :number, :desc => N_("content view numeric identifier"), :required => true
    param :environment_id, :number, :desc => N_("environment numeric identifier"), :required => true
    def remove_from_environment
      unless @content_view.environments.include?(@environment)
        fail HttpErrors::BadRequest, _("Content view '%{view}' is not in lifecycle environment '%{env}'.") %
              {view: @content_view.name, env: @environment.name}
      end

      task = async_task(::Actions::Katello::ContentView::RemoveFromEnvironment, @content_view, @environment)
      respond_for_async :resource => task
    end

    api :PUT, "/content_views/:id/remove", N_("Remove versions and/or environments from a content view and reassign systems and keys")
    param :id, :number, :desc => N_("content view numeric identifier"), :required => true
    param :environment_ids, Array, of: :number, :desc => N_("environment numeric identifiers to be removed")
    param :content_view_version_ids, Array, of: :number, :desc => N_("content view version identifiers to be deleted")
    param :system_content_view_id, :number, :desc => N_("content view to reassign orphaned systems to")
    param :system_environment_id, :number, :desc => N_("environment to reassign orphaned systems to")
    param :key_content_view_id, :number, :desc => N_("content view to reassign orphaned activation keys to")
    param :key_environment_id, :number, :desc => N_("environment to reassign orphaned activation keys to")
    param :destroy_content_view, :boolean, :desc => N_("delete the content view with all the versions and environments")
    def remove
      if params[:destroy_content_view]
        cv_envs = @content_view.content_view_environments
        versions = @content_view.versions
      else
        cv_envs = ContentViewEnvironment.where(:environment_id => params[:environment_ids],
                                               :content_view_id => params[:id]
        )
        versions = @content_view.versions.where(:id => params[:content_view_version_ids])
      end

      if !params[:destroy_content_view] && cv_envs.empty? && versions.empty?
        fail _("There either were no environments nor versions specified or there were invalid environments/versions specified. "\
               "Please check environment_ids and content_view_version_ids parameters.")
      end

      options = params.slice(:system_content_view_id,
                             :system_environment_id,
                             :key_content_view_id,
                             :key_environment_id,
                             :destroy_content_view
                            ).reject { |_k, v| v.nil? }.to_unsafe_h
      options[:content_view_versions] = versions
      options[:content_view_environments] = cv_envs

      task = async_task(::Actions::Katello::ContentView::Remove, @content_view, options)
      respond_for_async :resource => task
    end

    api :PUT, "/content_views/:id/bulk_delete_versions", N_("Bulk remove versions from a content view and reassign systems and keys")
    param_group :bulk_content_view_version_ids
    param :id, :number, :desc => N_("content view numeric identifier"), :required => true
    param :system_content_view_id, :number, :desc => N_("content view to reassign orphaned systems to")
    param :system_environment_id, :number, :desc => N_("environment to reassign orphaned systems to")
    param :key_content_view_id, :number, :desc => N_("content view to reassign orphaned activation keys to")
    param :key_environment_id, :number, :desc => N_("environment to reassign orphaned activation keys to")
    def bulk_delete_versions
      if @content_view.rolling?
        fail HttpErrors::BadRequest, _("It's not possible to bulk remove versions from a rolling content view.")
      end
      params[:bulk_content_view_version_ids] ||= {}

      versions = find_bulk_items(bulk_params: params[:bulk_content_view_version_ids],
                                 model_scope: ::Katello::ContentViewVersion.where(content_view_id: @content_view.id),
                                 key: :id)
      cv_envs = ContentViewEnvironment.where(:content_view_version_id => versions.pluck(:id),
                                             :content_view_id => @content_view.id
      )

      if !params[:destroy_content_view] && cv_envs.empty? && versions.empty?
        fail _("There either were no environments nor versions specified or there were invalid environments/versions specified. "\
               "Please check environment_ids and content_view_version_ids parameters.")
      end

      options = params.slice(:system_content_view_id,
                             :system_environment_id,
                             :key_content_view_id,
                             :key_environment_id
      ).reject { |_k, v| v.nil? }.to_unsafe_h
      options[:content_view_versions] = versions
      options[:content_view_environments] = cv_envs
      task = async_task(::Actions::Katello::ContentView::Remove, @content_view, options)
      respond_for_async :resource => task
    end

    api :PUT, "/content_views/:id/remove_filters", N_("Delete multiple filters from a content view")
    param :id, :number, :desc => N_("content view numeric identifier"), :required => true
    param :filter_ids, Array, of: :number, :desc => N_("filter identifiers"), :required => true
    def remove_filters
      Katello::ContentViewFilter.where(id: params[:filter_ids]).try(:destroy_all)
      respond_for_show(:resource => @content_view)
    end

    api :DELETE, "/content_views/:id", N_("Delete a content view")
    param :id, :number, :desc => N_("content view numeric identifier"), :required => true
    def destroy
      task = async_task(::Actions::Katello::ContentView::Destroy, @content_view)
      respond_for_async :resource => task
    end

    api :POST, "/content_views/:id/copy", N_("Make copy of a content view")
    param :id, :number, :desc => N_("Content view numeric identifier"), :required => true
    param :name, String, :required => true, :desc => N_("New content view name")
    def copy
      @content_view = Katello::ContentView.readable.find_by(:id => params[:id])
      if @content_view.rolling?
        fail HttpErrors::BadRequest, _("It's not possible to copy a rolling content view.")
      end
      throw_resource_not_found(name: 'content_view', id: params[:id]) if @content_view.blank?
      ensure_non_default
      new_content_view = @content_view.copy(params[:content_view][:name])
      respond_for_create :resource => new_content_view
    end

    private

    def validate_publish_params!
      if @content_view.rolling?
        fail HttpErrors::BadRequest, _("It's not possible to publish a rolling content view.")
      end
      if params[:repos_units].present? && @content_view.composite?
        fail HttpErrors::BadRequest, _("Directly setting package lists on composite content views is not allowed. Please " \
                                     "update the components, then re-publish the composite.")
      end
      if params[:major].present? && params[:minor].present? && ContentViewVersion.find_by(:content_view_id => params[:id], :major => params[:major], :minor => params[:minor]).present?
        fail HttpErrors::BadRequest, _("A CV version already exists with the same major and minor version (%{major}.%{minor})") % {:major => params[:major], :minor => params[:minor]}
      end

      if params[:major].present? && params[:minor].nil? || params[:major].nil? && params[:minor].present?
        fail HttpErrors::BadRequest, _("Both major and minor parameters have to be used to override a CV version")
      end

      cv_needs_publish = @content_view.needs_publish?
      if (::Foreman::Cast.to_bool(params[:publish_only_if_needed]) && !cv_needs_publish.nil? && !cv_needs_publish)
        fail HttpErrors::BadRequest, _("Content view does not need a publish since there are no audited changes since the last publish." \
                                     " Pass check_needs_publish parameter as false if you don't want to check if content view needs a publish.")
      end
    end

    def  ensure_non_default
      if @content_view.default? && !%w(show history).include?(params[:action])
        fail HttpErrors::BadRequest, _("The default content view cannot be edited, published, or deleted.")
      end
    end

    def  ensure_non_generated
      if @content_view.import_only?
        fail HttpErrors::BadRequest, _("Import only Content Views cannot be directly publsihed. Content can only be updated by importing into the view.")
      end

      if @content_view.generated?
        fail HttpErrors::BadRequest, _("Generated content views cannot be directly published. They can updated only via export.")
      end
    end

    def view_params
      attrs = [:name, :description, :auto_publish, :solve_dependencies, :import_only,
               :default, :created_at, :updated_at, :next_version, {:component_ids => []}]
      attrs.push(:label, :composite, :rolling) if action_name == "create"
      if (!@content_view || !@content_view.composite?)
        attrs.push({:repository_ids => []}, :repository_ids)
      end
      result = {}
      result = params.require(:content_view).permit(*attrs).to_h unless action_name == "update" && @content_view.rolling? && params[:content_view].empty?
      # sanitize repository_ids to be a list of integers
      result[:repository_ids] = result[:repository_ids].compact.map(&:to_i) if result[:repository_ids].present?
      result
    end

    def sanitized_environment_ids
      params[:environment_ids]&.compact&.map(&:to_i)
    end

    def validate_environment_ids!(rolling)
      if params[:environment_ids] && !rolling
        fail HttpErrors::BadRequest, _("It's not possible to provide environment_ids for anything other than a rolling content view.")
      end
    end

    def find_environment
      return if !params.key?(:environment_id) && params[:action] == "index"
      @environment = KTEnvironment.readable.find(params[:environment_id])
    end

    def add_use_latest_records(module_records, selected_latest_versions)
      module_records.group_by(&:author).each_pair do |_author, records|
        top_rec = records[0]
        latest = top_rec.dup
        latest.version = _("Always Use Latest (currently %{version})") % { version: latest.version }
        latest.pulp_id = nil
        module_records.delete(top_rec) if selected_latest_versions.include?(top_rec.pulp_id)
        module_records.push(latest)
      end
      module_records
    end
  end
end
