module ZuoraConnect::Controllers::Helpers

def authenticate_app_api_request

def authenticate_app_api_request
  #Skip session for api requests
  Thread.current[:appinstance] = nil
  request.session_options[:skip] = true
  ElasticAPM.set_tag(:trace_id, request.uuid) if defined?(ElasticAPM) && ElasticAPM.running?
  start_time = Time.now
  if request.headers["API-Token"].present?
    @appinstance = ZuoraConnect::AppInstance.where(:api_token => request.headers["API-Token"]).first
    ZuoraConnect.logger.debug("[#{@appinstance.id}] API REQUEST - API token") if @appinstance.present?
    check_instance
  elsif ZuoraConnect::AppInstance::INTERNAL_HOSTS.include?(request.headers.fetch("HOST", nil))
    zuora_host, zuora_entity_id, zuora_instance_id = [request.headers['zuora-host'], request.headers['zuora-entity-ids'].gsub('-',''), request.headers['zuora-instance-id']]
    #Validate host present
    if zuora_host.blank?
      render json: {"status": 401, "message": "zuora-host header was not supplied."}, status: :unauthorized
      return
    end
    #Validate entity-ids present
    if zuora_entity_id.blank?
      render json: {"status": 401, "message": "zuora-entity-ids header was not supplied."}, status: :unauthorized
      return
    end
    #Select with instance id if present. Used where mulitple deployments are done.
    if zuora_instance_id.present?
      appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host AND id = :id", entities: [zuora_entity_id], host: zuora_host, id: zuora_instance_id)
    else
      appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host", entities: [zuora_entity_id], host: zuora_host)
    end
    if appinstances.size == 0
      render json: {"status": 401, "message": "Missing mapping or no deployment for '#{zuora_host}-#{zuora_entity_id}' ."}, status: :unauthorized
    elsif appinstances.size > 1
      render json: {"status": 401, "message": "More than one app instance binded to host and entity ids. Please indicate correct instance via 'zuora-instance-id' header"}, status: :unauthorized
    else
      @appinstance = appinstances.first
    end
  else #if request.headers.fetch("Authorization", "").include?("Basic ")
    authenticate_or_request_with_http_basic do |username, password|
      @appinstance = ZuoraConnect::AppInstance.where(:token => password).first
      @appinstance ||= ZuoraConnect::AppInstance.where(:api_token => password).first
      ZuoraConnect.logger.debug("[#{@appinstance.id}] API REQUEST - Basic Auth") if @appinstance.present?
      check_instance
    end
  # else
  #   check_instance
  end
  
  if @appinstance.present?
    ZuoraConnect.logger.debug("[#{@appinstance.id}] Authenticate App API Request Completed In - #{(Time.now - start_time).round(2)}s")
  end
end

def authenticate_connect_app_request

def authenticate_connect_app_request
  ElasticAPM.set_tag(:trace_id, request.uuid) if defined?(ElasticAPM) && ElasticAPM.running?
  Thread.current[:appinstance] = nil
  if request.headers['Zuora-Auth-Token'].present?
    #Do we need to refresh session identity
    zuora_host = request.headers["HTTP_X_FORWARDED_HOST"] || "apisandbox.zuora.com"
    if request.headers["Zuora-Auth-Token"].present?
      zuora_client = ZuoraAPI::Oauth.new(url: "https://#{zuora_host}", bearer_token: request.headers["Zuora-Auth-Token"], oauth_session_expires_at: Time.now + 5.minutes )
    else
      zuora_client = ZuoraAPI::Basic.new(url: "https://#{zuora_host}", session: cookies['ZSession'])
    end
    zuora_entity_id = request.headers['ZuoraCurrentEntity']
    zuora_instance_id = params[:sidebar_launch].to_bool ? nil : (params[:app_instance_id] || session["appInstance"])
    #Identity blank or current entity different
    if (session["ZuoraCurrentIdentity"].blank? || session["ZuoraCurrentEntity"] != zuora_entity_id)
      begin
        identity, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("identity"))
        session["ZuoraCurrentIdentity"] = identity
        session["ZuoraCurrentEntity"] = identity['entityId']
        raise ZuoraConnect::Exceptions::Error.new("Header entity id, '#{zuora_entity_id}' does not match identity call entity id, '#{identity['entityId']}'.") if zuora_entity_id != identity['entityId']
      rescue => ex
        ZuoraConnect.logger.error(ex)
        render "zuora_connect/static/invalid_launch_request", :locals => {:exception => ex}
        return              
      end
    end
    #Find matching app instances.
    if zuora_instance_id.present?
      appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host AND id = :id", entities: [zuora_entity_id], host: zuora_client.rest_domain, id: zuora_instance_id).pluck(:id, :name)
    else
      #if app_instance_ids is present then permissions still controlled by connect
      if params[:app_instance_ids].present? 
        begin
          navbar, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("navigation"))
          urls = navbar['menus'].map {|x| x['url']}
          app_env = ENV["DEIS_APP"] || "xyz123"
          url = urls.compact.select {|url| File.basename(url).start_with?(app_env + '?')}.first
          task_ids = JSON.parse(Base64.urlsafe_decode64(CGI.parse(URI.parse(url).query)["app_instance_ids"][0]))
          
          appinstances = ZuoraConnect::AppInstance.where(:id => task_ids).pluck(:id, :name)
        rescue => ex
          ZuoraConnect.logger.error(ex)
          render "zuora_connect/static/invalid_launch_request", :locals => {:exception => ex}
          return
        end
      else
        appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host", entities: [zuora_entity_id], host: zuora_client.rest_domain).pluck(:id, :name)
      end
    end
    zuora_user_id = cookies['Zuora-User-Id'] || session["ZuoraCurrentIdentity"]['userId']
    #One deployed instance
    if appinstances.size == 1
      ZuoraConnect.logger.debug("Instance is #{appinstances.to_h.keys.first}")
      #Add user/update 
      user = ZuoraConnect::ZuoraUser.where(:zuora_user_id => zuora_user_id).first
      if user.present?
        ZuoraConnect.logger.debug("Current zuora user #{zuora_user_id}")
        if user.updated_at < Time.now - 1.day
          user.zuora_identity_response[zuora_entity_id] = session["ZuoraCurrentIdentity"]
          user.save!
        end
      else
        ZuoraConnect.logger.debug("New zuora user object for #{zuora_user_id}")
        user = ZuoraConnect::ZuoraUser.create!(:zuora_user_id => zuora_user_id, :zuora_identity_response => {zuora_entity_id => session["ZuoraCurrentIdentity"]})
      end 
      #Update access if admin in tenant
      if session["ZuoraCurrentIdentity"]['platformRole'] == 'ADMIN' && !user.app_permissions['access'].to_bool
        user.app_permissions['access'] = true 
        user.save!
      end
      #If user has has access to application
      if user.app_permissions['access'].to_bool || !ZuoraConnect.configuration.app_access_permissions
        session["appInstance"] = appinstances.to_h.keys.first
      else
        Thread.current[:appinstance] = nil
        session["appInstance"] = nil
        admin_users = ZuoraConnect::ZuoraUser.select("zuora_identity_response #>> '{#{zuora_entity_id},username}' as username").where("zuora_identity_response #>> :selector = 'ADMIN' ", :selector => "{#{zuora_entity_id},platformRole}")
        render "zuora_connect/static/permission_error", :locals => {:admins => admin_users}
        return
      end
    #We have multiple, user must pick
    elsif appinstances.size > 1
      ZuoraConnect.logger.debug("User must select instance. #{@names}")
      render "zuora_connect/static/launch", :locals => {:names => appinstances.to_h}
      return
    else 
      begin
        #Ensure user can access oauth creation API 
        if session["ZuoraCurrentIdentity"]['platformRole'] != 'ADMIN'
          raise ZuoraConnect::Exceptions::Error.new("User is not admin, workflow cannot be deployed.")
        end
        
        body = {
          'clientId' => SecureRandom.uuid, 
          'clientSecret' => SecureRandom.hex(10), 
          'userId' => zuora_user_id, 
          'entityIds' => [zuora_entity_id.unpack("a8a4a4a4a12").join('-')], 
          'customAuthorities' => [], 
          'additionalInformation' => {
            'description' => 'This user is for workflow application.', 
            'name' => 'Workflow API User'
          }
        }
        oauth_response, response = zuora_client.rest_call(method: :post, body: body.to_json, url: zuora_client.rest_endpoint("genesis/clients").gsub('v1/', ''), session_type: :bearer)
        new_zuora_client = ZuoraAPI::Oauth.new(url: "https://#{zuora_host}", oauth_client_id: oauth_response["clientId"], oauth_secret: oauth_response["clientSecret"] )
        
        client_describe, response = new_zuora_client.rest_call(body: body.to_json, url: zuora_client.rest_endpoint("genesis/user/info").gsub('v1/', ''), session_type: :bearer)
        Apartment::Tenant.switch!("public")
        next_id = (ZuoraConnect::AppInstance.all.where(:access_token => nil).order(id: :desc).limit(1).pluck(:id).first || 24999999) + 1
        begin
          Apartment::Tenant.create(next_id.to_s)
        rescue Apartment::TenantExists => ex
          ZuoraConnect.logger.debug("Tenant Already Exists")
        end
        task_data = {
          "id": next_id,
          "name": client_describe["tenantName"],
          "mode": "Collections",
          "status": "Running",
          "target_login": {
              "tenant_type": "Zuora",
              "username": session["ZuoraCurrentIdentity"]["username"],
              "url": new_zuora_client.url,
              "status": "Active",
              "oauth_client_id": body['clientId'],
              "oauth_secret": body['clientSecret'],
              "authentication_type": "OAUTH",
              "entities": client_describe["accessibleEntities"].map {|e| e.merge({'displayName' => client_describe["tenantName"]})} #needs work
          },
          "tenant_ids": client_describe["accessibleEntities"].map{|e| e['entityId'] }.push(client_describe["tenantId"]).uniq,
        }
        appinstance = ZuoraConnect::AppInstance.new(:id => next_id, :zuora_logins => task_data.to_json, :oauth_expires_at => Time.now + 1000.years)
        appinstance.save(:validate => false)
        session["appInstance"] = appinstance.id
      rescue => ex
        ZuoraConnect.logger.error(ex)
        render "zuora_connect/static/invalid_launch_request", :locals => {:exception => ex}
        return
      end
    end
  end
  start_time = Time.now
  if ZuoraConnect.configuration.mode == "Production"
    if request["data"] && /^([A-Za-z0-9+\/\-\_]{4})*([A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)$/.match(request["data"].to_s)
      setup_instance_via_data
    else
      setup_instance_via_session
    end
  else
    setup_instance_via_dev_mode
  end
  #Call .data_lookup with the current session to retrieve session. In some cases session may be stored/cache in redis 
  #so data lookup provides a model method that can be overriden per app.
  if params[:controller] != 'zuora_connect/api/v1/app_instance' && params[:action] != 'drop'
    if @appinstance.new_session_for_ui_requests(:params => params)
      @appinstance.new_session(:session => @appinstance.data_lookup(:session => session))
    end
  end
  if session["#{@appinstance.id}::user::email"].present? 
    ElasticAPM.set_user(session["#{@appinstance.id}::user::email"])  if defined?(ElasticAPM) && ElasticAPM.running?
    PaperTrail.whodunnit =  session["#{@appinstance.id}::user::email"] if defined?(PaperTrail)
  end
  begin
    I18n.locale = session["#{@appinstance.id}::user::locale"] ?  session["#{@appinstance.id}::user::locale"] : @appinstance.locale
  rescue I18n::InvalidLocale => ex
    ZuoraConnect.logger.error(ex) if !ZuoraConnect::AppInstance::IGNORED_LOCALS.include?(ex.locale.to_s.downcase)
  end
  Time.zone = session["#{@appinstance.id}::user::timezone"] ? session["#{@appinstance.id}::user::timezone"] : @appinstance.timezone
  ZuoraConnect.logger.debug("[#{@appinstance.blank? ? "N/A" : @appinstance.id}] Authenticate App Request Completed In - #{(Time.now - start_time).round(2)}s")
end

def check_connect_admin

def check_connect_admin
  return session["#{@appinstance.id}::admin"]
end

def check_connect_admin!

def check_connect_admin!
  raise ZuoraConnect::Exceptions::AccessDenied.new("User is not an authorized admin for this application") if !session["#{@appinstance.id}::admin"]
end

def check_instance

API ONLY
def check_instance
  if defined?(@appinstance) && @appinstance.present?
    if @appinstance.new_session_for_api_requests(:params => params)
      @appinstance.new_session(:session => @appinstance.data_lookup(:session => session))
    end
    Thread.current[:appinstance] = @appinstance
    PaperTrail.whodunnit = "API User" if defined?(PaperTrail)
    ElasticAPM.set_user("API User")  if defined?(ElasticAPM) && ElasticAPM.running?
    return true
  else
    response.set_header('WWW-Authenticate', "Basic realm=\"Application\"")
    #render json: {"status": 401, "message": "Access Denied"}, status: :unauthorized
    render html: "HTTP Basic: Access denied.\n", status: :unauthorized
    render plain: "Access Denied", status: :unauthorized
  end
end

def persist_connect_app_session

def persist_connect_app_session
  if @appinstance.present?
    if defined?(Redis.current)
      @appinstance.cache_app_instance
    else
      session.merge!(@appinstance.save_data)
    end
  end
end

def setup_instance_via_data

def setup_instance_via_data
  session.clear
  values = JSON.parse(ZuoraConnect::AppInstance.decrypt_response(Base64.urlsafe_decode64(request["data"])))
  if values["param_data"]
    values["param_data"].each do |k ,v|
      params[k] = v
    end
  end
  session["#{values["appInstance"]}::destroy"] = values["destroy"]
  session["appInstance"] = values["appInstance"]
  if values["current_user"]
    session["#{values["appInstance"]}::admin"] = values["current_user"]["admin"] ? values["current_user"]["admin"] : false
    session["#{values["appInstance"]}::user::timezone"] = values["current_user"]["timezone"]
    session["#{values["appInstance"]}::user::locale"] = values["current_user"]["locale"]
    session["#{values["appInstance"]}::user::email"] = values["current_user"]["email"]
  end
  ZuoraConnect.logger.debug({msg: 'Setup values', connect: values}) if Rails.env != "production"
  @appinstance = ZuoraConnect::AppInstance.where(:id => values["appInstance"].to_i).first
  if @appinstance.blank?
    Apartment::Tenant.switch!("public")
    begin
      Apartment::Tenant.create(values["appInstance"].to_s)
    rescue Apartment::TenantExists => ex
      ZuoraConnect.logger.debug("Tenant Already Exists")
    end
    @appinstance = ZuoraConnect::AppInstance.new(:api_token =>  values[:api_token],:id => values["appInstance"].to_i, :access_token => values["access_token"].blank? ? values["user"] : values["access_token"], :token => values["refresh_token"]  , :refresh_token => values["refresh_token"].blank? ? values["key"] : values["refresh_token"], :oauth_expires_at => values["expires"])
    @appinstance.save(:validate => false)
  else
    @appinstance.access_token = values["access_token"] if !values["access_token"].blank? && @appinstance.access_token != values["access_token"]
    @appinstance.refresh_token = values["refresh_token"] if !values["refresh_token"].blank? && @appinstance.refresh_token != values["refresh_token"]
    @appinstance.oauth_expires_at = values["expires"] if !values["expires"].blank?
    @appinstance.api_token = values["api_token"] if !values["api_token"].blank? && @appinstance.api_token != values["api_token"]
    if @appinstance.access_token_changed? && @appinstance.refresh_token_changed?
      @appinstance.save(:validate => false)
    else
      raise ZuoraConnect::Exceptions::AccessDenied.new("Authorization mistmatch. Possible tampering")
    end
  end     
end

def setup_instance_via_dev_mode

def setup_instance_via_dev_mode
  session["appInstance"] = ZuoraConnect.configuration.dev_mode_appinstance
  user = ZuoraConnect.configuration.dev_mode_user
  key = ZuoraConnect.configuration.dev_mode_pass
  values = {:user => user , :key => key, :appinstance => session["appInstance"]}
  @appinstance = ZuoraConnect::AppInstance.where(:id => values[:appinstance].to_i).first
  if @appinstance.blank?
    Apartment::Tenant.switch!("public")
    begin
      Apartment::Tenant.create(values[:appinstance].to_s)
    rescue Apartment::TenantExists => ex
      Apartment::Tenant.drop(values[:appinstance].to_s)
      retry
    end
    @appinstance = ZuoraConnect::AppInstance.new(:id => values[:appinstance].to_i, :access_token => values[:user], :refresh_token => values[:key], :token => "#{values[:key]}#{values[:key]}", :api_token => "#{values[:key]}#{values[:key]}")
    @appinstance.save(:validate => false)
  end
  if @appinstance.access_token.blank? || @appinstance.refresh_token.blank? || @appinstance.token.blank? || @appinstance.api_token.blank?
    @appinstance.update_attributes!(:access_token =>  values["user"], :refresh_token =>  values["key"], :token => "#{values[:key]}#{values[:key]}", :api_token => "#{values[:key]}#{values[:key]}")
  end
  session["#{@appinstance.id}::admin"] =  ZuoraConnect.configuration.dev_mode_admin
end

def setup_instance_via_session

def setup_instance_via_session
  if session["appInstance"].present?
    @appinstance = ZuoraConnect::AppInstance.where(:id => session["appInstance"]).first
  else
    raise ZuoraConnect::Exceptions::SessionInvalid.new("Session Blank -- Relaunch Application")
  end
end