module ZuoraConnect::Controllers::Helpers

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
  start_time = Time.now
  if ZuoraConnect.configuration.mode == "Production"
    zuora_entity_id = request.headers['ZuoraCurrentEntity'] || cookies['ZuoraCurrentEntity']
    if zuora_entity_id.present?
      zuora_tenant_id = cookies['Zuora-Tenant-Id']
      zuora_user_id = cookies['Zuora-User-Id']
      zuora_host = request.headers["HTTP_X_FORWARDED_HOST"] || "apisandbox.zuora.com"
      zuora_details = {'host' => zuora_host, 'user_id' => zuora_user_id, 'tenant_id' => zuora_tenant_id, 'entity_id' => zuora_entity_id}
      #Do we need to refresh session identity
      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 )
      elsif cookies['ZSession'].present?
        zuora_client = ZuoraAPI::Basic.new(url: "https://#{zuora_host}", session: cookies['ZSession'])
      else
        render "zuora_connect/static/error_handled", :locals => {
          :title => "Missing Authorization Token", 
          :message => "Zuora 'Zuora-Auth-Token' header and 'ZSession' cookie not present."
        }, :layout => false
        return
      end
      begin
        zuora_instance_id = params[:sidebar_launch].to_s.to_bool ? nil : (params[:app_instance_id] || session["appInstance"])
        #Identity blank or current entity different
        different_zsession = session["ZSession"] != cookies['ZSession']
        missmatched_entity = session["ZuoraCurrentEntity"] != zuora_entity_id
        missing_identity = session["ZuoraCurrentIdentity"].blank?
        if (missing_identity || missmatched_entity || different_zsession)
          zuora_details.merge!({'identity' => {'different_zsession' => different_zsession, 'missing_identity' => missing_identity, 'missmatched_entity' => missmatched_entity}})
          identity, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("identity"))
          session["ZuoraCurrentIdentity"] = identity
          session["ZuoraCurrentEntity"] = identity['entityId']
          session["ZSession"] = cookies['ZSession']
          zuora_instance_id = nil
          zuora_details["identity"]["entityId"] = identity['entityId']
          client_describe, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("genesis/user/info").gsub('v1/', ''), session_type: zuora_client.class == ZuoraAPI::Oauth ? :bearer : :basic, headers: zuora_client.class == ZuoraAPI::Oauth ? {} : {'Authorization' => "ZSession-a3N2w #{zuora_client.get_session(prefix: false, auth_type: :basic)}"})
          session["ZuoraCurrentUserInfo"] = client_describe
        
          raise ZuoraConnect::Exceptions::Error.new("Header entity id does not match identity call entity id.") if zuora_entity_id != identity['entityId']
        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? 
            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)
          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}")
          @appinstance = ZuoraConnect::AppInstance.find(appinstances.to_h.keys.first)
          #Add user/update 
          @zuora_user = ZuoraConnect::ZuoraUser.where(:zuora_user_id => zuora_user_id).first
          if @zuora_user.present?
            ZuoraConnect.logger.debug("Current zuora user #{zuora_user_id}")
            if @zuora_user.updated_at < Time.now - 1.day
              @zuora_user.zuora_identity_response[zuora_entity_id] = session["ZuoraCurrentIdentity"]
              @zuora_user.save!
            end
          else
            ZuoraConnect.logger.debug("New zuora user object for #{zuora_user_id}")
            @zuora_user = ZuoraConnect::ZuoraUser.create!(:zuora_user_id => zuora_user_id, :zuora_identity_response => {zuora_entity_id => session["ZuoraCurrentIdentity"]})
          end 
          @zuora_user.session = session
          session["#{@appinstance.id}::user::email"] = session['ZuoraCurrentIdentity']["username"]
          session["#{@appinstance.id}::user::timezone"] = session['ZuoraCurrentIdentity']["timeZone"]
          session["#{@appinstance.id}::user::locale"] = session['ZuoraCurrentIdentity']["language"]
          session["appInstance"] = @appinstance.id
        #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}, :layout => false
          return
        #We have no deployed instance for this tenant
        else 
          #Ensure user can access oauth creation API 
          if session["ZuoraCurrentIdentity"]['platformRole'] != 'ADMIN' 
            Thread.current[:appinstance] = nil
            session["appInstance"] = nil
            render "zuora_connect/static/error_handled", :locals => {
              :title => "Application can only complete its initial setup via platform administrator", 
              :message => "Please contact admin of tenant and have them click on link again to launch application."
            }, :layout => false
            return
          end
          Apartment::Tenant.switch!("public")
          ActiveRecord::Base.transaction do
            ActiveRecord::Base.connection.execute('LOCK public.zuora_users IN ACCESS EXCLUSIVE MODE')
            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)
            if appinstances.size > 0
              redirect_to "https://#{zuora_host}/apps/newlogin.do?retURL=#{request.fullpath}"
              return
            end
            next_id = (ZuoraConnect::AppInstance.all.where('id > 24999999').order(id: :desc).limit(1).pluck(:id).first || 24999999) + 1
            user = (ENV['DEIS_APP'] || "Application").split('-').map(&:capitalize).join(' ')
            body = {
              'userId' => zuora_user_id, 
              'entityIds' => [zuora_entity_id.unpack("a8a4a4a4a12").join('-')], 
              'customAuthorities' => [], 
              'additionalInformation' => {
                'description' => "This user is for #{user} application.", 
                'name' => "#{user} API User #{next_id}"
              }
            }
            oauth_response, response = zuora_client.rest_call(method: :post, body: body.to_json, url: zuora_client.rest_endpoint("genesis/clients").gsub('v1/', ''), session_type: zuora_client.class == ZuoraAPI::Oauth ? :bearer : :basic, headers: zuora_client.class == ZuoraAPI::Oauth ? {} : {'Authorization' => "ZSession-a3N2w #{zuora_client.get_session(prefix: false, auth_type: :basic)}"})
            new_zuora_client = ZuoraAPI::Oauth.new(url: "https://#{zuora_host}", oauth_client_id: oauth_response["clientId"], oauth_secret: oauth_response["clientSecret"] )
            if session["ZuoraCurrentUserInfo"].blank?
              client_describe, response = new_zuora_client.rest_call(url: zuora_client.rest_endpoint("genesis/user/info").gsub('v1/', ''), session_type: :bearer)
            else
              client_describe = session["ZuoraCurrentUserInfo"]
            end
            available_entities = client_describe["accessibleEntities"].select {|entity| entity['id'] == zuora_entity_id}
            task_data = {
              "id": next_id,
              "name": client_describe["tenantName"],
              "mode": "Collections",
              "status": "Running",
              ZuoraConnect::AppInstance::LOGIN_TENANT_DESTINATION => {
                "tenant_type": "Zuora",
                "username": session["ZuoraCurrentIdentity"]["username"],
                "url": new_zuora_client.url,
                "status": "Active",
                "oauth_client_id": oauth_response['clientId'],
                "oauth_secret": oauth_response['clientSecret'],
                "authentication_type": "OAUTH",
                "entities": available_entities.map {|e| e.merge({'displayName' => client_describe["tenantName"]})}
              },
              "tenant_ids": available_entities.map{|e| e['entityId']}.uniq,
            }
            mapped_values = {:id => next_id, :api_token => rand(36**64).to_s(36), :token => rand(36**64).to_s(36), :zuora_logins => task_data, :oauth_expires_at => Time.now + 1000.years, :zuora_domain => zuora_client.rest_domain, :zuora_entity_ids => [zuora_entity_id]}
            @appinstance = ZuoraConnect::AppInstance.new(mapped_values)
            retry_count = 0
            begin
              @appinstance.save(:validate => false)
            rescue ActiveRecord::RecordNotUnique => ex
              if (retry_count += 1) < 3
                @appinstance.assign_attributes({:api_token => rand(36**64).to_s(36), :token => rand(36**64).to_s(36)})
                retry
              else
                Thread.current[:appinstance] = nil
                session["appInstance"] = nil
                render "zuora_connect/static/error_handled", :locals => {
                  :title => "Application could not create unique tokens.", 
                  :message => "Please contact support or retry launching application."
                }, :layout => false
                return
              end
            end
          end
          Apartment::Tenant.switch!("public")
          begin
            Apartment::Tenant.create(@appinstance.id.to_s)
          rescue Apartment::TenantExists => ex
            ZuoraConnect.logger.debug("Tenant Already Exists")
          end
          @appinstance.refresh
          session["appInstance"] = @appinstance.id
        end
      rescue ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError => ex
        output_xml, input_xml = zuora_client.soap_call(errors: [], z_session: false) do |xml|
          xml['api'].getUserInfo
        end
        final_error = output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text
        session.clear
        ZuoraConnect.logger.warn(ex, zuora: zuora_details.merge({:error => final_error}))
        redirect_to "https://#{zuora_host}/apps/newlogin.do?retURL=#{request.fullpath}"
        return
      rescue => ex
        ZuoraConnect.logger.error(ex, zuora: zuora_details)
        render "zuora_connect/static/error_unhandled", :locals => {:exception => ex}, :layout => false
        return              
      end
    elsif 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
      if session["appInstance"].present?
        @appinstance = ZuoraConnect::AppInstance.where(:id => session["appInstance"]).first 
      else
        render "zuora_connect/static/error_handled", :locals => {
          :title => "Application state could not be verified", 
          :message => "Please relaunch application."
        }, :layout => false
        return 
      end
    end
  else
    setup_instance_via_dev_mode
  end
  if !defined?(@appinstance) || @appinstance.blank?
    render "zuora_connect/static/error_handled", :locals => {
      :title => "Application state could not be found.", 
      :message => "Please relaunch application."
    }, :layout => false
    return 
  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
    locale = session["#{@appinstance.id}::user::locale"]
    I18n.locale = locale.present? ? locale : @appinstance.locale
  rescue I18n::InvalidLocale => ex
    ZuoraConnect.logger.error(ex) if !ZuoraConnect::AppInstance::IGNORED_LOCALS.include?(ex.locale.to_s.downcase)
  end
  begin
    Time.zone = session["#{@appinstance.id}::user::timezone"] ? session["#{@appinstance.id}::user::timezone"] : @appinstance.timezone
  rescue 
    ZuoraConnect.logger.error(ex) 
  end
  ZuoraConnect.logger.debug("[#{@appinstance.blank? ? "N/A" : @appinstance.id}] Authenticate App Request Completed In - #{(Time.now - start_time).round(2)}s")
end