require 'active_support/core_ext/object/to_query'

module ActiveShipping
  class NewZealandPost < Carrier
    cattr_reader :name
    @@name = "New Zealand Post"

    URL = "http://api.nzpost.co.nz/ratefinder"

    def requirements
      [:key]
    end

    def find_rates(origin, destination, packages, options = {})
      options = @options.merge(options)
      request = RateRequest.from(origin, destination, packages, options)
      request.raw_responses = commit(request.urls) if request.new_zealand_origin?
      request.rate_response
    end

    protected

    def commit(urls)
      save_request(urls).map { |url| ssl_get(url) }
    end

    def self.default_location
      Location.new(
        :country => "NZ",
        :city => "Wellington",
        :address1 => "22 Waterloo Quay",
        :address2 => "Pipitea",
        :postal_code => "6011"
      )
    end

    class NewZealandPostRateResponse < RateResponse
      attr_reader :raw_responses

      def initialize(success, message, params = {}, options = {})
        @raw_responses = options[:raw_responses]
        super
      end
    end

    class RateRequest
      attr_reader :urls
      attr_writer :raw_responses

      def self.from(*args)
        return International.new(*args) unless domestic?(args[0..1])
        Domestic.new(*args)
      end

      def initialize(origin, destination, packages, options)
        @origin = Location.from(origin)
        @destination = Location.from(destination)
        @packages = Array(packages).map { |package| NewZealandPostPackage.new(package, api) }
        @params = { :format => "json", :api_key => options[:key] }
        @test = options[:test]
        @rates = @responses = @raw_responses = []
        @urls = @packages.map { |package| url(package) }
      end

      def rate_response
        @rates = rates
        NewZealandPostRateResponse.new(true, "success", response_params, response_options)
      rescue => error
        NewZealandPostRateResponse.new(false, error.message, response_params, response_options)
      end

      def new_zealand_origin?
        self.class.new_zealand?(@origin)
      end

      protected

      def self.new_zealand?(location)
        ['NZ', nil].include?(Location.from(location).country_code)
      end

      def self.domestic?(locations)
        locations.select { |location| new_zealand?(location) }.size == 2
      end

      def response_options
        {
          :rates => @rates,
          :raw_responses => @raw_responses,
          :request => @urls,
          :test => @test
        }
      end

      def response_params
        { :responses => @responses }
      end

      def rate_options(products)
        {
          :total_price => products.sum { |product| price(product) },
          :currency => "NZD",
          :service_code => products.first["code"]
        }
      end

      def rates
        rates_hash.map do |service, products|
          RateEstimate.new(@origin, @destination, NewZealandPost.name, service, rate_options(products))
        end
      end

      def rates_hash
        products_hash.select { |_service, products| products.size == @packages.size }
      end

      def products_hash
        product_arrays.flatten.group_by { |product| service_name(product) }
      end

      def product_arrays
        responses.map do |response|
          raise(response["message"]) unless response["status"] == "success"
          response["products"]
        end
      end

      def responses
        @responses = @raw_responses.map { |response| parse_response(response) }
      end

      def parse_response(response)
        JSON.parse(response)
      end

      def url(package)
        "#{URL}/#{api}?#{params(package).to_query}"
      end

      def params(package)
        @params.merge(api_params).merge(package.params)
      end
    end

    class Domestic < RateRequest
      def service_name(product)
        [product["service_group_description"], product["description"]].join(" ")
      end

      def api
        :domestic
      end

      def api_params
        {
          :postcode_src => @origin.postal_code,
          :postcode_dest => @destination.postal_code,
          :carrier => "all"
        }
      end

      def price(product)
        product["cost"].to_f
      end
    end

    class International < RateRequest
      def rates
        raise "New Zealand Post packages must originate in New Zealand" unless new_zealand_origin?
        super
      end

      def service_name(product)
        [product["group"], product["name"]].join(" ")
      end

      def api
        :international
      end

      def api_params
        { :country_code => @destination.country_code }
      end

      def price(product)
        product["price"].to_f
      end
    end

    class NewZealandPostPackage
      def initialize(package, api)
        @package = package
        @api = api
        @params = { :weight => weight, :length => length }
      end

      def params
        @params.merge(api_params).merge(shape_params)
      end

      protected

      def weight
        @package.kg
      end

      def length
        mm(:length)
      end

      def height
        mm(:height)
      end

      def width
        mm(:width)
      end

      def shape
        return :cylinder if @package.cylinder?
        :cuboid
      end

      def api_params
        send("#{@api}_params")
      end

      def international_params
        { :value => value }
      end

      def domestic_params
        {}
      end

      def shape_params
        send("#{shape}_params")
      end

      def cuboid_params
        { :height => height, :thickness => width }
      end

      def cylinder_params
        { :diameter => width }
      end

      def mm(measurement)
        @package.cm(measurement) * 10
      end

      def value
        return 0 unless @package.value && currency == "NZD"
        @package.value / 100
      end

      def currency
        @package.currency || "NZD"
      end
    end
  end
end