require 'json'
require 'net/http'
require 'pilfer/profile'
require 'uri'

module Pilfer
  class Server
    attr_accessor :uri, :token

    def initialize(uri, token, options = {})
      @uri   = URI.parse(uri)
      @token = token
      @async = options[:async] || true
      @on_error = options[:on_error] || lambda { |ex| $stdout.puts "Pilfer::Server Exception: #{ex.class}: #{ex.message}:\n#{ex.backtrace.join("\n\t")}" }
    end

    def write(profile_data, profile_start, description, options = {})
      async   = (options[:submit] || :sync) == :async
      details = { 'hostname'     => Socket.gethostname,
                  'pid'          => Process.pid,
                  'description'  => description,
                  'file_sources' => file_sources_for_profile(profile_data) }

      payload = RbLineProfFormat.
                  profile_to_json(profile_data, profile_start).
                  merge(details)

      if async
        Thread.new(payload) do |payload|
          submit_profile payload
        end
      else
        submit_profile payload
      end
    end

    private

    def submit_profile(payload)
      request = Net::HTTP::Post.new('/api/profiles')
      request.content_type = 'application/json'
      request['Authorization'] = %{Token token="#{token}"}
      request.body = JSON.generate(payload)

      http = Net::HTTP.new(uri.host, uri.port)

      if uri.scheme == 'https'
        http.verify_mode = OpenSSL::SSL::VERIFY_PEER
        http.use_ssl = true
        store = OpenSSL::X509::Store.new
        store.set_default_paths
        http.cert_store = store
      end

      case (response = http.start {|http| http.request(request) })
      when Net::HTTPSuccess, Net::HTTPRedirection
      else
        response.error!
      end
    rescue Exception => ex
      @on_error.call(ex)
    end

    def file_sources_for_profile(profile_data)
      profile_data.each_with_object({}) {|(file, _), sources|
        sources[file] = File.exists?(file) ? File.read(file) : nil
      }
    end
  end
end

# Formatting a profile as JSON may eventually be provided by rblineprof.
class RbLineProfFormat
  def self.profile_to_json(profile_data, profile_start)
    files = profile_data.each_with_object({}) do |(file, lines), files|
      profile_lines = lines[1..-1].
                        each_with_index.
                        each_with_object({}) do |(data, number), inner_lines|
        next unless data.any? {|datum| datum > 0 }
        wall_time, cpu_time, calls = data
        inner_lines[number] = { 'wall_time' => wall_time,
                          'cpu_time'  => cpu_time,
                          'calls'     => calls }
      end

      total, child, exclusive, total_cpu, child_cpu, exclusive_cpu = lines[0]

      files[file] = { 'total'         => total,
                      'child'         => child,
                      'exclusive'     => exclusive,
                      'total_cpu'     => total_cpu,
                      'child_cpu'     => child_cpu,
                      'exclusive_cpu' => exclusive_cpu,
                      'lines'         => profile_lines }
    end

    {
      'profile' => {
        'timestamp' => profile_start.to_i,
        'files'     => files
      }
    }
  end
end
