# Filters added to this controller will be run for all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

require 'api_error'

class ApplicationController < ActionController::Base
  include Pundit::Authorization

  protect_from_forgery

  include ActionController::ImplicitRender
  include ActionController::MimeResponds
  include FlipperFeature

  include Authenticator
  include RescueHandler
  include RescueAuthorizationHandler
  include SetCurrentRequestDetails
  include BackendProxy

  # session :disabled => true

  @skip_validation = false

  before_action :add_api_version

  before_action :set_influxdb_data
  before_action :shutup_rails
  before_action :validate_params
  before_action :require_login

  before_action :validate_xml_request
  after_action :validate_xml_response if CONFIG['response_schema_validation'] == true

  def pundit_user
    User.session
  end

  # FIXME: This is only a helper class for User authorization. This should happen in User.
  def permissions
    @user_permissions ||= Suse::Permission.new(User.possibly_nobody)
  end

  # Method for mapping actions in a controller to (XML) schemas based on request
  # method (GET, PUT, POST, etc.). Example:
  #
  # class UserController < ActionController::Base
  #   # Validation on request data is performed based on the request type and the
  #   # provided schema name. Validation for a GET request only checks the XML response,
  #   # whereas a POST request may want to check the (user-supplied) request as well as the
  #   # own response to the request.
  #
  #   validate_action :index => {:method => :get, :response => :users}
  #   validate_action :edit =>  {:method => :put, :request => :user, :response => :status}
  #
  #   def index
  #     # return all users ...
  #   end
  #
  #   def edit
  #     if @request.put?
  #       # request data has already been validated here
  #     end
  #   end
  # end
  def self.validate_action(opt)
    opt.each do |action, action_opt|
      Suse::Validator.add_schema_mapping(controller_path, action, action_opt)
    end
  end

  protected

  def validate_params
    params.each do |key, value|
      next if value.nil?
      next if key == 'xmlhash' # perfectly fine
      raise InvalidParameterError, "Parameter #{key} has non String class #{value.class}" unless value.is_a?(String)
    end
  end

  def require_user_or_scmsync_host_check
    return if User.session

    require_scmsync_host_check
  end

  def require_scmsync_host_check
    scm_cookie = request.env['HTTP_X_SCM_BRIDGE_COOKIE']
    raise MissingParameterError, 'X-SCM_BRIDGE_COOKIE is not set' if scm_cookie.blank?
    raise MissingParameterError, 'Incorrect scm bridge cookie' if scm_cookie != CONFIG['scm_bridge_cookie'].to_s
  end

  def add_api_version
    response.headers['X-Opensuse-APIVersion'] = CONFIG['version'].to_s
  end

  def gather_exception_defaults(opt)
    if opt[:message]
      @summary = opt[:message].to_s
    elsif @exception
      @summary = @exception.message
    end

    @exception = opt[:exception]
    @errorcode = opt[:errorcode]

    @status = if opt[:status]
                opt[:status].to_i
              else
                400
              end

    response.headers['WWW-Authenticate'] = 'basic realm="API login"' if @status == 401 && !response.headers['WWW-Authenticate']
    if @status == 404
      @summary ||= 'Not found'
      @errorcode ||= 'not_found'
    end

    @summary ||= 'Internal Server Error'

    @errorcode ||= if @exception
                     'uncaught_exception'
                   else
                     'unknown'
                   end
  end

  def render_error(opt = {})
    # avoid double render error
    self.response_body = nil
    gather_exception_defaults(opt)

    response.headers['X-Opensuse-Errorcode'] = @errorcode
    respond_to do |format|
      format.xml { render template: 'status', status: @status }
      format.json { render json: { errorcode: @errorcode, summary: @summary }, status: @status }
      format.html do
        flash[:error] = "#{@summary} (#{@errorcode})" unless request.env['HTTP_REFERER']
        redirect_back_or_to root_path
      end
    end
  end

  def render_ok(opt = {})
    # keep compatible to old call style
    @errorcode = 'ok'
    @summary = 'Ok'
    @data = opt[:data] if opt[:data]
    render template: 'status', status: :ok
  end

  def render_invoked(opt = {})
    @errorcode = 'invoked'
    @summary = 'Job invoked'
    @data = opt[:data] if opt[:data]
    render template: 'status', status: :ok
  end

  # Passes control to subroutines determined by action and a request parameter. By
  # default the parameter assumed to contain the command is ':cmd'. Looks for a method
  # named <action>_<command>
  #
  # Example:
  #
  # If you call dispatch_command from an action 'index' with the query parameter cmd
  # having the value 'show', it will call the method 'index_show'
  #
  def dispatch_command(action, cmd)
    cmd_handler = "#{action}_#{cmd}"
    logger.debug "dispatch_command: trying to call method '#{cmd_handler}'"
    __send__(cmd_handler)
  end

  def build_query_from_hash(hash, key_list = nil)
    Backend::Connection.build_query_from_hash(hash, key_list)
  end

  class LazyRequestReader
    def initialize(req)
      @req = req
    end

    def to_s
      @req.raw_post
    end
  end

  def validate_xml_request(method = nil)
    opt = params
    opt[:method] = method || request.method.to_s
    opt[:type] = 'request'
    logger.debug "Validate XML request: #{request.raw_post}"
    Suse::Validator.validate(opt, LazyRequestReader.new(request))
  end

  def validate_xml_response
    return if @skip_validation

    request_format = request.format != 'json'
    response_status = response.status.to_s[0..2] == '200'
    response_headers = response.headers['Content-Type'] !~ %r{.*/json}i && response.headers['Content-Disposition'] != 'attachment'

    return unless request_format && response_status && response_headers

    opt = params
    opt[:method] = request.method.to_s
    opt[:type] = 'response'
    ms = Benchmark.ms do
      if response.body.respond_to?(:call)
        sio = StringIO.new
        response.body.call(nil, sio) # send_file can return a block that takes |response, output|
        str = sio.string
      else
        str = response.body
      end
      Suse::Validator.validate(opt, str)
    end
    logger.debug "Validate XML response: #{response} took #{Integer(ms + 0.5)}ms"
  end

  def set_response_format_to_xml
    request.format = :xml if request.format == :html
  end

  private

  # A before_action to demand authentication
  def require_login
    return if User.session

    render_error status: 401, errorcode: 'authentication_required', message: 'Authentication Required'
  end

  # A before_action to demand the Admin role
  def require_admin
    return if User.possibly_nobody.admin?

    render_error status: 403, errorcode: 'admin_required', message: 'Admin Privileges Required'
  end

  def shutup_rails
    Rails.cache.silence! unless Rails.env.development?
  end

  def spider_request?
    return request.bot? if Rails.env.production?

    false
  end

  def set_influxdb_data
    in_beta = User.possibly_nobody.in_beta?

    tags = {
      beta: in_beta,
      anonymous: !User.session,
      spider: spider_request?,
      interconnect: false,
      interface: :api
    }
    if in_beta
      Flipper.preload_all.map(&:name).each do |feature_name|
        tags[:"beta_#{feature_name}"] = Flipper.enabled?(feature_name.to_sym, User.possibly_nobody)
      end
    end

    InfluxDB::Rails.current.tags = tags
  end
end
