module Katello
  # rubocop:disable Metrics/ClassLength
  class Api::Rhsm::CandlepinProxiesController < Api::V2::ApiController
    include Katello::Authentication::ClientAuthentication

    IF_MODIFIED_SINCE_HEADER = 'If-Modified-Since'.freeze

    before_action :disable_strong_params

    wrap_parameters false

    around_action :repackage_message
    before_action :find_host, :only => [:consumer_show, :consumer_destroy, :consumer_checkin, :enabled_repos,
                                        :regenerate_identity_certificates, :facts,
                                        :available_releases, :serials, :upload_tracer_profile]
    before_action :authorize, :only => [:consumer_create, :list_owners, :rhsm_index]
    before_action :authorize_client_or_user, :only => [:consumer_show, :regenerate_identity_certificates, :upload_tracer_profile, :facts, :proxy_jobs_get_path]
    before_action :authorize_client_or_admin, :only => [:hypervisors_update, :async_hypervisors_update, :hypervisors_heartbeat]
    before_action :authorize_proxy_routes, :only => [:get, :post, :put, :delete]
    before_action :authorize_client, :only => [:consumer_destroy, :consumer_checkin,
                                               :enabled_repos, :available_releases]

    before_action :check_registration_services, :only => [:consumer_create, :consumer_destroy, :consumer_activate]

    before_action :add_candlepin_version_header

    before_action :proxy_request_path, :proxy_request_body
    before_action :set_organization_id, :except => [:hypervisors_update, :async_hypervisors_update]
    before_action :find_hypervisor_organization, :only => [:async_hypervisors_update, :hypervisors_update]

    before_action :check_media_type, :except => :async_hypervisors_update

    prepend_before_action :convert_owner_to_organization_id, :except => [:hypervisors_update, :async_hypervisors_update], :if => lambda { params.key?(:owner) }
    prepend_before_action :convert_organization_label_to_id, :only => [:rhsm_index, :consumer_activate, :consumer_create], :if => lambda { params.key?(:organization_id) }

    def repackage_message
      yield
    ensure
      if response.status >= 400
        begin
          body_json = JSON.parse(response.body)
          if body_json['message'] && body_json['displayMessage'].nil?
            body_json['displayMessage'] = body_json['message']
          end
          response.body = body_json.to_s
        rescue JSON::ParserError
          # Not a json response, leave as-is
        end
      end
    end

    rescue_from RestClient::Exception do |e|
      Rails.logger.error(pp_exception(e, with_backtrace: false))
      Rails.logger.error(e.backtrace.detect { |line| line.match("katello.*controller") })
      if request_from_katello_cli?
        render :json => { :errors => [e.http_body] }, :status => e.http_code
      else
        render :plain => e.http_body, :status => e.http_code
      end
    end

    def proxy_request_path
      @request_path = drop_api_namespace(@_request.fullpath)
    end

    def proxy_request_body
      @request_body = @_request.body
    end

    def drop_api_namespace(original_request_path)
      prefix = "/rhsm"
      original_request_path.gsub(prefix, '')
    end

    def get
      extra_headers = {}
      modified_since = request.headers[IF_MODIFIED_SINCE_HEADER]
      if modified_since.present?
        extra_headers[IF_MODIFIED_SINCE_HEADER] = modified_since
      end

      r = Resources::Candlepin::Proxy.get(@request_path, extra_headers)
      logger.debug filter_sensitive_data(r)
      render :json => r, :status => r.code
    end

    def delete
      r = Resources::Candlepin::Proxy.delete(@request_path, @request_body.read)
      logger.debug filter_sensitive_data(r)
      render :json => r
    end

    def post
      r = Resources::Candlepin::Proxy.post(@request_path, @request_body.read)
      logger.debug filter_sensitive_data(r)
      render :json => r
    end

    def put
      r = Resources::Candlepin::Proxy.put(@request_path, @request_body.read)
      logger.debug filter_sensitive_data(r)
      render :json => r
    end

    #api :GET, "/consumers/:id", N_("Show a system")
    #param :id, String, :desc => N_("UUID of the consumer"), :required => true
    def consumer_show
      render :json => Resources::Candlepin::Consumer.get(params[:id])
    end

    #api :GET, "/owners/:organization_id/environments", N_("List environments for RHSM")
    def rhsm_index
      @all_environments = get_content_view_environments(params[:name], Organization.current).collect do |env|
        {
          :id => env.cp_id,
          :name => env.label,
          :display_name => env.name,
          :description => env.content_view.description,
        }
      end

      respond_for_index :collection => @all_environments
    end

    #api :POST, "/hypervisors/OWNER"
    # Note that this request comes in as content-type 'text/plain' so that
    # tomcat won't parse the json.  Here we just pass the plain body through to candlepin
    def async_hypervisors_update
      task = Katello::Resources::Candlepin::Consumer.async_hypervisors(owner: params[:owner],
                                                                       reporter_id: params[:reporter_id],
                                                                       raw_json: request.raw_post)
      async_task(::Actions::Katello::Host::Hypervisors, nil, :task_id => task['id'])

      render :json => task
    end

    #api :POST, "/hypervisors", N_("Update the hypervisors information for environment")
    #desc 'See virt-who tool for more details.'
    def hypervisors_update
      login = User.consumer? ? User.anonymous_api_admin.login : User.current.login
      task = User.as(login) do
        params['owner'] = @organization.label #override owner label if
        params['env'] = nil #hypervisors don't need an environment
        sync_task(::Actions::Katello::Host::Hypervisors, params.except(:controller, :action, :format).to_h)
      end
      render :json => task.output[:results]
    end

    def hypervisors_heartbeat
      render json: Katello::Resources::Candlepin::Consumer.hypervisors_heartbeat(owner: params[:owner], reporter_id: params[:reporter_id])
    end

    #api :PUT, "/consumers/:id/checkin/", N_("Update consumer check-in time")
    #param :date, String, :desc => N_("check-in time")
    def consumer_checkin
      @host.update(:last_checkin => params[:date])
      Candlepin::Consumer.new(@host.subscription_facet.uuid, @host.organization.label).checkin(params[:date])
      render :json => Resources::Candlepin::Consumer.get(@host.subscription_facet.uuid)
    end

    api :PUT, "/consumers/:id/tracer", N_("Update services requiring restart")
    param :traces, Hash, :required => true
    def upload_tracer_profile
      User.as_anonymous_admin do
        @host.import_tracer_profile(params[:traces])
      end
      render json: { displayMessage: _("Tracer profile uploaded successfully") }
    end

    def available_releases
      render :json => @host.content_facet.try(:available_releases) || []
    end

    def list_owners
      orgs = User.current.allowed_organizations
      # rhsm expects owner (Candlepin format)
      respond_for_index :collection => orgs.map { |o| { :key => o.label, :displayName => o.name } }
    end

    #api :POST, "/consumers/:id", N_("Regenerate consumer identity")
    #param :id, String, :desc => N_("UUID of the consumer")
    #desc 'Schedules the consumer identity certificate regeneration'
    def regenerate_identity_certificates
      uuid = @host.subscription_facet.uuid
      Candlepin::Consumer.new(uuid, @host.organization.label).regenerate_identity_certificates
      render :json => Resources::Candlepin::Consumer.get(uuid)
    end

    api :PUT, "/systems/:id/enabled_repos", N_("Update the information about enabled repositories")
    desc <<-DESC
      Used to keep the information about enabled repositories up to date.
      This information is then used for computing the errata available for the system.
    DESC
    param :enabled_repos, Hash, :required => true do
      param :repos, Array, :required => true do
        param :baseurl, Array, :desc => N_("List of enabled repo urls for the repo (Only first is used.)"), :required => false
      end
    end
    param :id, String, :desc => N_("UUID of the system"), :required => true
    def enabled_repos
      repos_params = params.dig('enabled_repos', 'repos')
      fail(HttpErrors::BadRequest, _("The request did not contain any repository information.")) if repos_params.nil?

      result = nil
      User.as_anonymous_admin do
        result = @host.import_enabled_repositories(repos_params)
      end

      respond_for_show :resource => result
    end

    #api :POST, "/environments/:environment_id/consumers", N_("Register a consumer in environment")
    def consumer_create
      host = Katello::RegistrationManager.process_registration(rhsm_params, find_content_view_environments)

      host.reload

      update_host_registered_through(host, request.headers)

      render :json => Resources::Candlepin::Consumer.get(host.subscription_facet.uuid)
    end

    #api :DELETE, "/consumers/:id", N_("Unregister a consumer")
    #param :id, String, :desc => N_("UUID of the consumer"), :required => true
    def consumer_destroy
      User.as_anonymous_admin do
        Katello::RegistrationManager.unregister_host(@host, :unregistering => !Setting['unregister_delete_host'])
      end
      render :plain => _("Deleted consumer '%s'") % params[:id], :status => :no_content
    end

    # used for registering with activation keys
    #api :POST, "/consumers", N_("Register a system with activation key (compatibility)")
    #param :activation_keys, String, :required => true
    def consumer_activate
      # Activation keys are userless by definition so use the internal generic user
      # Set it before calling find_activation_keys to allow communication with candlepin
      User.current = User.anonymous_admin
      additional_set_taxonomy
      activation_keys = find_activation_keys

      host = Katello::RegistrationManager.process_registration(rhsm_params, nil, activation_keys)

      update_host_registered_through(host, request.headers)
      host.reload

      render :json => Resources::Candlepin::Consumer.get(host.subscription_facet.uuid)
    end

    #api :GET, "/status", N_("Shows version information")
    #description N_("This service is available for unauthenticated users")
    def server_status
      candlepin_response = Resources::Candlepin::CandlepinPing.ping
      candlepin_response[:managerCapabilities] << 'combined_reporting'
      render :json => candlepin_response
    end

    #api :PUT, "/consumers/:id", N_("Update consumer information")
    def facts
      User.current = User.anonymous_admin
      @host.update_candlepin_associations(rhsm_params)
      update_environments_from_facts(params[:environments]) if params[:environments]
      update_host_registered_through(@host, request.headers)
      @host.refresh_statuses([::Katello::RhelLifecycleStatus])
      render :json => {:content => _("Facts successfully updated.")}, :status => :ok
    end

    def serials
      @host.subscription_facet.last_checkin = Time.now
      @host.subscription_facet.save!
      render :json => Katello::Resources::Candlepin::Consumer.serials(@host.subscription_facet.uuid)
    end

    def get_parent_host(headers)
      hostnames = headers["HTTP_X_FORWARDED_HOST"]
      host = hostnames.split(/[,,:]/)[0].strip if hostnames
      host || URI.parse(Setting[:foreman_url]).host
    end

    def get_content_source_id(hostname)
      proxies = SmartProxy.unscoped.authorized.filter do |sp|
        hostname == URI.parse(sp.url).hostname
      end
      return nil if proxies.length != 1
      proxies.first.id
    end

    private

    # in case set taxonomy from core was skipped since the User.current was nil at that moment (typically AK was used instead of username/password)
    # we need to set proper context, unfortunately params[:organization_id] is already overridden again by set_organization_id so we need to
    # set correct parameter value and then reset it back
    def additional_set_taxonomy
      params[:organization_id], original = find_organization(:owner).id, params[:organization_id] if params[:owner].present?
      set_taxonomy
      params[:organization_id] = original if params[:owner].present?
    end

    def disable_strong_params
      params.permit!
    end

    def deny_access
      fail HttpErrors::Forbidden, 'Access denied'
    end

    def set_organization_id
      params[:organization_id] = params[:owner] if params[:owner]
    end

    def find_host(uuid = nil)
      uuid ||= params[:id]
      facet = Katello::Host::SubscriptionFacet.where(:uuid => uuid).first
      if facet.nil?
        # check with candlepin if consumer is Gone, raises RestClient::Gone
        User.as_anonymous_admin { Resources::Candlepin::Consumer.get(uuid) }
      end
      @host = ::Host::Managed.unscoped.find(facet.host_id)
    end

    def find_content_view_environments
      environments = []

      if params.key?(:environment_id)
        environments = [get_content_view_environment("cp_id", params[:environment_id])]
      elsif params.key?(:environments)
        if params['environments'].length > 1 && !Setting['allow_multiple_content_views']
          fail HttpErrors::BadRequest, _('Registering to multiple environments is not enabled.')
        end
        environments = params[:environments].map do |env|
          get_content_view_environment("cp_id", env['id'])
        end
      elsif params.key?(:organization_id) && !params.key?(:environment_id) && !params.key?(:environments)
        organization = Organization.current
        environments = [organization.library.content_view_environment]
      elsif User.current.default_organization.present?
        environments = [User.current.default_organization.library.content_view_environment]
      else
        fail HttpErrors::NotFound, _("User '%s' did not specify an organization ID and does not have a default organization.") % current_user.login
      end
      environments
    end

    def find_hypervisor_organization
      if User.consumer?
        host = find_host(User.current.uuid)
        @organization = host.organization
      elsif params[:owner]
        @organization = Organization.find_by(:label => params[:owner])
      end
      deny_access unless @organization
    end

    def find_organization(key = :organization_id)
      organization = nil

      if params.key?(key)
        label = params[key].is_a?(ActionController::Parameters) ? params[key]['key'] : params[key]
        organization = Organization.find_by(:label => label)
      end

      if organization.nil?
        message = _("Couldn't find Organization '%s'.")
        fail HttpErrors::NotFound, message % params[key]
      end

      if User.current && !User.consumer? && !User.current.allowed_organizations.include?(organization)
        message = _("User '%{user}' does not belong to Organization '%{organization}'.")
        fail HttpErrors::NotFound, message % {:user => current_user.login, :organization => params[key]}
      end

      organization
    end

    def convert_organization_label_to_id
      params[:organization_id] = find_organization.id
    end

    def convert_owner_to_organization_id
      params[:organization_id] = find_organization(:owner).id
    end

    def find_activation_keys
      organization = Organization.current

      if (ak_names = params[:activation_keys])
        fail HttpErrors::NotFound, _("Organization not found") if organization.nil?
        ak_names        = ak_names.split(",").uniq.compact
        activation_keys = ak_names.map do |ak_name|
          activation_key = organization.activation_keys.find_by(:name => ak_name)
          fail HttpErrors::NotFound, _("Couldn't find activation key '%s'") % ak_name unless activation_key

          if activation_key.multi_content_view_environment? && !Setting['allow_multiple_content_views']
            fail HttpErrors::BadRequest, _("Activation key '%s' is associated to multiple environments"\
                                           " and registering to multiple environments is not enabled.") % ak_name
          end

          if !activation_key.unlimited_hosts && activation_key.usage_count >= activation_key.max_hosts
            fail Errors::MaxHostsReachedException, _("Max Hosts (%{limit}) reached for activation key '%{name}'") %
                { :limit => activation_key.max_hosts, :name => activation_key.name }
          end
          activation_key
        end
      else
        activation_keys = []
      end
      if activation_keys.empty?
        fail HttpErrors::BadRequest, _("At least one activation key must be provided")
      end
      activation_keys
    end

    def update_environments_from_facts(param_environments)
      return if param_environments.blank?
      new_envs = param_environments.map do |env|
        get_content_view_environment("cp_id", env['id'])
      end
      new_envs.compact!
      Rails.logger.debug "Setting new content view environments for host #{@host.to_label}: #{new_envs.map(&:label)}"
      @host.content_facet.content_view_environments = new_envs
    rescue ::Katello::Errors::MultiEnvironmentNotSupportedError => e
      raise HttpErrors::BadRequest, e.message
    end

    def get_content_view_environment(key, value)
      cve = nil
      if value
        cve = ContentViewEnvironment.where(key => value).first
        fail HttpErrors::NotFound, _("Couldn't find environment '%s'") % value unless cve
        deny_access unless cve.readable? || User.consumer?
      end
      cve
    end

    def get_content_view_environments(label = nil, organization = nil)
      organization ||= @organization

      environments = ContentViewEnvironment.joins(:content_view => :organization).
          where("#{Organization.table_name}.id = ?", organization.id)
      environments = environments.where("#{Katello::ContentViewEnvironment.table_name}.label = ?", label) if label

      environments.reject do |env|
        (env.content_view.default && !env.environment.readable?) ||
            !env.content_view.readable? ||
            env.content_view.generated_for_repository?
      end
    end

    def rhsm_params
      params.slice(:name, :type, :facts, :installedProducts, :autoheal, :releaseVer, :usage, :role, :serviceLevel, :uuid, :capabilities, :guestIds, :lastCheckin).to_h
    end

    def logger
      ::Foreman::Logging.logger('katello/cp_proxy')
    end

    def respond_for_index(options = {})
      collection = options[:collection] || resource_collection
      status     = options[:status] || :ok
      format     = options[:format] || :json

      render format => collection, :status => status
    end

    def respond_for_show(options = {})
      resource = options[:resource] || resource
      status   = options[:status] || :ok
      format   = options[:format] || :json

      render format => resource, :status => status
    end

    def authorize_client_or_user
      client_authorized? || authorize
    end

    def authorize_client_or_admin
      if !client_authorized? && !authorize
        deny_access
      end
    end

    def authorize_client
      deny_access unless client_authorized?
    end

    def client_authorized?
      authorized = authenticate_client && User.consumer?
      authorized = (User.current.uuid == @host.subscription_facet.uuid) if @host && User.consumer?
      authorized
    end

    def update_host_registered_through(host, headers)
      parent_host = get_parent_host(headers)
      host.subscription_facet.update_attribute(:registered_through, parent_host)
      set_host_content_source(host, parent_host)
    end

    def registering_thru_load_balancer?(hostname)
      ::SmartProxy.behind_load_balancer(hostname).present?
    end

    def set_host_content_source(host, content_source_hostname)
      content_source_id = get_content_source_id(content_source_hostname)
      if registering_thru_load_balancer?(content_source_hostname)
        Rails.logger.info "Host %s registered through load balancer %s" % [host.name, content_source_hostname]
        content_source_id = ::SmartProxy.behind_load_balancer(content_source_hostname)&.first&.id
      end
      Rails.logger.warn "Host %s registered through unknown proxy %s" % [host.name, content_source_hostname] if content_source_id.nil?
      host.content_facet.update_attribute(:content_source_id, content_source_id)
    end

    def check_registration_services
      fail "Unable to register system, not all services available" unless Katello::RegistrationManager.check_registration_services
    end

    # rubocop:disable Metrics/MethodLength
    # rubocop:disable Metrics/CyclomaticComplexity
    def authorize_proxy_routes
      deny_access unless (authenticate || authenticate_client)

      route, params = Engine.routes.router.recognize(request) do |rte, parameters|
        break rte, parameters if rte.name
      end

      # route names are defined in routes.rb (:as => :name)
      case route.name
      when "rhsm_proxy_consumer_deletionrecord_delete_path"
        User.consumer? || Organization.deletable?
      when "rhsm_proxy_owner_pools_path"
        if params[:consumer]
          User.consumer? && current_user.uuid == params[:consumer]
        else
          User.consumer? || ::User.current.can?(:view_organizations, self)
        end
      when "rhsm_proxy_owner_servicelevels_path", "rhsm_proxy_owner_system_purpose_path"
        (User.consumer? || ::User.current.can?(:view_organizations, self))
      when "rhsm_proxy_consumer_accessible_content_path", "rhsm_proxy_consumer_certificates_path",
           "rhsm_proxy_consumer_releases_path", "rhsm_proxy_certificate_serials_path",
           "rhsm_proxy_consumer_entitlements_path", "rhsm_proxy_consumer_entitlements_post_path",
           "rhsm_proxy_consumer_entitlements_delete_path", "rhsm_proxy_consumer_entitlements_pool_delete_path",
           "rhsm_proxy_consumer_certificates_put_path", "rhsm_proxy_consumer_dryrun_path",
           "rhsm_proxy_consumer_owners_path", "rhsm_proxy_consumer_compliance_path", "rhsm_proxy_consumer_purpose_compliance_path"
        User.consumer? && current_user.uuid == params[:id]
      when "rhsm_proxy_consumer_certificates_delete_path"
        User.consumer? && current_user.uuid == params[:consumer_id]
      when "rhsm_proxy_pools_path"
        User.consumer? && current_user.uuid == params[:consumer]
      when "rhsm_proxy_subscriptions_post_path"
        User.consumer? && current_user.uuid == params[:consumer_uuid]
      when "rhsm_proxy_consumer_content_overrides_path", "rhsm_proxy_consumer_content_overrides_put_path",
           "rhsm_proxy_consumer_content_overrides_delete_path",
           "rhsm_proxy_consumer_guestids_path", "rhsm_proxy_consumer_guestids_get_guestid_path",
           "rhsm_proxy_consumer_guestids_put_path", "rhsm_proxy_consumer_guestids_put_guestid_path",
           "rhsm_proxy_consumer_guestids_delete_guestid_path",
           "rhsm_proxy_entitlements_path"
        # These queries are restricted in Candlepin
        User.consumer?
      when "rhsm_proxy_deleted_consumers_path"
        current_user.admin?
      when "rhsm_proxy_jobs_get_path"
        User.consumer? || current_user.admin?
      else
        Rails.logger.warn "Unknown proxy route #{request.method} #{request.fullpath}, access denied"
        deny_access
      end
    end
  end
end
