# require "set"
require_relative "authentication"
require_relative "configurable"
require_relative "arguments"
require_relative "util"
require_relative 'rate_limit'
require_relative 'project'
require_relative 'incident'

require_relative 'client/account'
require_relative 'client/bookmarks'
require_relative 'client/categories'
require_relative 'client/incidents'
require_relative 'client/notifications'
require_relative 'client/projects'
require_relative 'client/rate_limit'
require_relative 'client/users'
require_relative 'client/companies'
require_relative 'client/attachments'


module Nearmiss

  class Client
    include Nearmiss::Util
    include Nearmiss::Authentication
    include Nearmiss::Configurable

    include Nearmiss::Client::Account
    include Nearmiss::Client::Bookmarks
    include Nearmiss::Client::Categories
    include Nearmiss::Client::Incidents
    include Nearmiss::Client::Notifications
    include Nearmiss::Client::Projects
    include Nearmiss::Client::RateLimit
    include Nearmiss::Client::Users
    include Nearmiss::Client::Companies
    include Nearmiss::Client::Attachments

    # include Nearmiss::Client::Users
    # include Nearmiss::Client::ProjectLibrary
    # include Nearmiss::Client::Projects
    # include Nearmiss::Client::Templates
    # include Nearmiss::Client::Checklists
    # include Nearmiss::Client::Tasks
    # include Nearmiss::Client::Issues
    # include Nearmiss::Client::Utils

    attr_accessor :access_token, :client_id, :uid, :expiry, :me

    CONVENIENCE_HEADERS = Set.new([:accept, :content_type])


    def initialize(options = {})

      # Use options passed in, but fall back to module defaults
      Nearmiss::Configurable.keys.each do |key|
        instance_variable_set(:"@#{key}", options[key] || Nearmiss.instance_variable_get(:"@#{key}"))
      end

      sign_in if basic_authenticated?
    end

    # Compares client options to a Hash of requested options
    #
    # @param opts [Hash] Options to compare with current client options
    # @return [Boolean]
    def same_options?(opts)
      opts.hash == options.hash
    end


    def inspect # :nodoc:

      inspected = super

      # mask password
      inspected = inspected.gsub! @password, "*******" if @password
      # Only show last 4 of token, secret
      if @access_token
        inspected = inspected.gsub! @access_token, "#{'*'*36}#{@access_token[36..-1]}"
      end
      # if @client_secret
      #   inspected = inspected.gsub! @client_secret, "#{'*'*36}#{@client_secret[36..-1]}"
      # end

      inspected
    end

    # Hypermedia agent for the BIM360-Field API
    #
    # @return [Sawyer::Agent]
    def agent
      @agent ||= Sawyer::Agent.new(api_endpoint, sawyer_options) do |http|
        # http.headers[:accept] = "image/jpg"
        http.headers[:content_type]         = "application/json"
        http.headers[:user_agent]           = user_agent
        http.headers[:accept]               = "application/json"
        http.headers[:api_key]              = api_key
        http.headers[:'x-client-platform']  = "api"

        if @access_token
          http.headers.merge!({
            :'access-token'   => @access_token,
            :client           => @client_id,
            :expiry           => @expiry,
            :'token-type'     => "Bearer",
            :uid              => @uid
          })
        end

        # if
        # if basic_authenticated?
          # http.basic_auth(@login, @password)
        # elsif token_authenticated?
        #   http.authorization 'token', @access_token
        # end
      end
    end


    # Set username for authentication
    #
    # @param value [String] Nearmiss-field username
    def email=(value)
      reset_agent
      @email = value
    end

    # Set password for authentication
    #
    # @param value [String] Nearmiss-field password
    def password=(value)
      reset_agent
      @password = value
    end

    # Set OAuth access token for authentication
    #
    # @param value [String] 40 character Nearmiss-field API OAuth access token
    # def access_token=(value)
    #   reset_agent
    #   @access_token = value
    # end





    # Make a HTTP GET request
    #
    # @param url [String] The path, relative to {#api_endpoint}
    # @param options [Hash] Query and header params for request
    # @return [Sawyer::Resource]
    def get(url, options = {})
      request :get, url, options
    end

    def post(url, options = {})
      request :post, url, options
    end

    def put(url, options = {})
      request :put, url, options
    end

    def patch(url, options = {})
      request :patch, url, options
    end

    def delete(url, options = {})
      request :delete, url, options
    end

    # Response for last HTTP request
    #
    # @return [Sawyer::Response]
    def last_response
      @last_response if defined? @last_response
    end


    # Make one or more HTTP GET requests, optionally fetching
    # the next page of results from URL in Link response header based
    # on value in {#auto_paginate}.
    #
    # @param url [String] The path, relative to {#api_endpoint}
    # @param options [Hash] Query and header params for request
    # @param block [Block] Block to perform the data concatination of the
    #   multiple requests. The block is called with two parameters, the first
    #   contains the contents of the requests so far and the second parameter
    #   contains the latest response.
    # @return [Sawyer::Resource]
    def paginate(url, options = {}, &block)
      opts = parse_query_and_convenience_headers(options.dup)
      if @auto_paginate || @per_page
        opts[:query][:per_page] ||=  @per_page || (@auto_paginate ? 100 : nil)
      end

      data = request(:get, url, opts)

      if @auto_paginate
        while @last_response.rels[:next] #&& rate_limit.remaining > 0
          @last_response = @last_response.rels[:next].get
          if block_given?
            yield(data, @last_response)
          else
            data.concat(@last_response.data) if @last_response.data.is_a?(Array)
          end
        end

      end

      data
    end


    # Wrapper around Kernel#warn to print warnings unless
    # OCTOKIT_SILENT is set to true.
    #
    # @return [nil]
    def nearmiss_warn(*message)
      unless ENV['NEARMISS_SILENT']
        warn message
      end
    end



    private

    def reset_agent
      @agent = nil
    end

    # Make a HTTP Request
    #
    # @param method [Symbol] Http method
    # @param path [String] path relative to {#api_endpoint}
    # @param options [Hash] Query and header params for request
    # @return [Sawyer::Resource]

    def request(method, path, data, options = {})
      if data.is_a?(Hash)
        options[:query]   = data.delete(:query) || {}
        options[:headers] = data.delete(:headers) || {}
        if accept = data.delete(:accept)
          options[:headers][:accept] = accept
        end
      end

      if @access_token
        options[:headers].merge!({
          "access-token"  => @access_token,
          "client"        => @client_id,
          "expiry"        => @expiry,
          "token-type"    => "Bearer",
          "uid"           => @uid
        })
      end


      @last_response = response = agent.call(method, URI::Parser.new.escape(path.to_s), data, options)

      update_headers(response.headers)

      response.data

    end


    def sawyer_options
      opts = {
        :links_parser => Sawyer::LinkParsers::Simple.new
      }
      conn_opts           = @connection_options
      conn_opts[:builder] = @middleware if @middleware
      conn_opts[:proxy]   = @proxy if @proxy
      opts[:faraday]      = Faraday.new(conn_opts)

      opts
    end

    # Executes the request, checking if it was successful
    #
    # @return [Boolean] True on success, false otherwise
    def boolean_from_response(method, path, options = {})
      request(method, path, options)
      @last_response.status == 204
    rescue Nearmiss::NotFound
      false
    end


    def parse_query_and_convenience_headers(options)
      headers = options.fetch(:headers, {})
      CONVENIENCE_HEADERS.each do |h|
        if header = options.delete(h)
          headers[h] = header
        end
      end
      query = options.delete(:query)
      opts = {:query => options}
      opts[:query].merge!(query) if query && query.is_a?(Hash)
      opts[:headers] = headers unless headers.empty?

      opts
    end


  end


end