# frozen_string_literal: true

require 'net/http'

begin
  require 'yajl'
rescue LoadError
  require 'json'
end

require 'timeout'

module Crunchbase
  class API
    SUPPORTED_ENTITIES = {
      'categories' => Model::Category,
      'organizations' => Model::OrganizationSummary,
      'organization' => Model::Organization,
      'people' => Model::PersonSummary,
      'person' => Model::Person,
      'products' => Model::ProductSummary,
      'ipos' => Model::Ipo,
      'funding_rounds' => Model::FundingRound,
      'funding-rounds' => Model::FundingRound,
      'acquisitions' => Model::Acquisition,
      'locations' => Model::Location,
      'offices' => Model::Office,
      'customers' => Model::Customer,
      'degrees' => Model::Degree,
      # 'experience' => nil,
      'primary_affiliation' => Model::PrimaryAffiliation,
      'videos' => Model::Video,
      'founded_companies' => Model::FoundedCompany,
      'primary_location' => Model::PrimaryLocation,
      'advisor_at' => Model::AdvisoryRole,
      'investors' => Model::Organization
    }.freeze

    @redirect_limit = 2
    @timeout = 60
    @debug = false

    # Must be overridden in subclasses
    RESOURCE_NAME         = 'undefined'.freeze
    RESOURCE_LIST         = 'undefineds'.freeze

    ORDER_CREATED_AT_ASC  = 'created_at ASC'.freeze
    ORDER_CREATED_AT_DESC = 'created_at DESC'.freeze
    ORDER_UPDATED_AT_ASC  = 'updated_at ASC'.freeze
    ORDER_UPDATED_AT_DESC = 'updated_at DESC'.freeze

    class << self
      attr_accessor :key # Necessary
      attr_accessor :debug, :timeout, :redirect_limit

      def api_url
        [API_BASE_URL, '/', API_VERSION, '/'].join
      end

      def single_entity(permalink, entity_name)
        raise CrunchException, 'Unsupported Entity Type' unless SUPPORTED_ENTITIES.keys.include?(entity_name)

        fetch(permalink, entity_name)
      end

      # Returns the JSON parser, whether that's an instance of Yajl or JSON
      def parser
        return Yajl::Parser if defined?(Yajl)

        JSON
      end

      # Fetches URI for the permalink interface.
      def fetch(permalink, kclass_name)
        get_json_response(api_url + "#{kclass_name}/#{permalink}")
      end

      # Fetches URI for the search interface.
      def search(options, resource_list)
        options[:page] = 1 if options[:page].nil?
        options[:order] = ORDER_CREATED_AT_ASC if options[:order].nil?

        uri = api_url + "#{resource_list}?" + collect_parameters(options)

        get_json_response(uri)
      end

      # Fetches URI for the search interface and adapts the payload.
      def batch_search(requests_array)
        uri = "#{api_url}batch"
        request_body = { requests: requests_array }

        post_json_response(uri, request_body)
      end

      # Fetches URI for the search interface.
      def list(options, resource_list)
        options[:page]  = 1 if options[:page].nil?
        model_name      = options.delete(:model_name) || SUPPORTED_ENTITIES[resource_list]

        uri = api_url + "#{resource_list}?" + collect_parameters(options)

        Model::Search.new options, get_json_response(uri), model_name
      end

      def collect_parameters(options)
        options.map { |k, v| "#{k}=#{v}" }.join('&')
      end

      def organization_lists(permalink, category, options)
        lists_for_category('organizations', permalink, category, options)
      end

      def person_lists(permalink, category, options)
        lists_for_category('people', permalink, category, options)
      end

      def funding_rounds_lists(permalink, category, options)
        lists_for_category('funding-rounds', permalink, category, options)
      end

      def lists_for_category(classify_name, permalink, category, options)
        options[:page]  = 1 if options[:page].nil?
        options[:order] = ORDER_CREATED_AT_ASC if options[:order].nil?
        model_name      = options.delete(:model_name)

        uri = api_url + "#{classify_name}/#{permalink}/#{category}?#{collect_parameters(options)}"

        Model::Search.new options, get_json_response(uri), model_name
      end

      # Gets specified URI, then parses the returned JSON. Raises Timeout error
      #   if request time exceeds set limit. Raises Exception if returned
      #   JSON contains an error.
      def get_json_response(uri)
        raise Exception, 'User key required, visit https://data.crunchbase.com/v3.1/docs' unless @key

        uri += "#{uri =~ /\?/ ? '&' : '?'}user_key=#{@key}"

        resp = Timeout.timeout(@timeout) do
          get_url_following_redirects(uri, @redirect_limit)
        end

        response = parser.parse(resp)
        response = response[0] if response.is_a?(Array)
        raise Exception, response['message'] unless response['message'].nil?

        response['data']
      end

      # Performs actual HTTP requests, recursively if a redirect response is
      # encountered. Will raise HTTP error if response is not 200, 404, or 3xx.
      def get_url_following_redirects(uri_str, limit = 10)
        raise Exception, 'HTTP redirect too deep' if limit.zero?

        uri = URI.parse(URI.encode(uri_str))

        debugging(uri)

        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = true if uri.scheme == 'https'
        response = http.start do |h|
          h.request Net::HTTP::Get.new(uri.request_uri, 'User-Agent' => 'crunchbase-ruby-library')
        end

        case response
        when Net::HTTPSuccess, Net::HTTPNotFound, Net::HTTPInternalServerError, Net::HTTPConflict
          response.body
        when Net::HTTPRedirection
          get_url_following_redirects(response['location'], limit - 1)
        else
          response.error!
        end
      end

      # Gets specified URI, and the object for request's body, then parses the returned JSON. Raises Timeout error
      #   if request time exceeds set limit. Raises Exception if returned
      #   JSON contains an error.
      def post_json_response(uri, request_body)
        raise Exception, 'User key required, visit https://data.crunchbase.com/v3.1/docs' unless @key

        body_string = request_body.to_json

        resp = Timeout.timeout(@timeout) do
          post_url_following_redirects(uri, body_string, @redirect_limit)
        end

        response = parser.parse(resp)
        raise Exception, response['message'] unless response['message'].nil?

        response['data']
      end

      # Performs actual HTTP requests, recursively if a redirect response is
      # encountered. Will raise HTTP error if response is not 200, 404, or 3xx.
      def post_url_following_redirects(uri_str, body_string, limit = 10)
        raise Exception, 'HTTP redirect too deep' if limit.zero?

        uri = URI.parse(URI.encode(uri_str))

        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = true if uri.scheme == 'https'

        req = Net::HTTP::Post.new(uri.request_uri)
        req.add_field('User-Agent', 'crunchbase-ruby-library')
        req.add_field('Content-Type', 'application/json')
        req.add_field('X-Cb-User-Key', @key)

        req.body = body_string

        response = http.start do |h|
          h.request req
        end

        case response
        when Net::HTTPSuccess, Net::HTTPNotFound, Net::HTTPInternalServerError, Net::HTTPConflict
          response.body
        when Net::HTTPRedirection
          post_url_following_redirects(response['location'], limit - 1)
        else
          response.error!
        end
      end

      def debugging(uri)
        return unless debug

        puts '*' * 140
        puts "***  #{uri}  ***"
        puts '*' * 140
      end
    end
  end
end
