require 'net/http'
require 'net/https'
require 'hpricot'
require 'ostruct'

module FEDEX
  class FedExError < StandardError
  end

  class Client
    def initialize(options)
      @prefs = options
      @logger = RAILS_DEFAULT_LOGGER
    end  

    def rate_list(to_zip, weight, boxes, home_delivery = false)
      response = self.rate_request(@prefs[:origin_zip], to_zip, weight, boxes, home_delivery)
      Hpricot(response).search('entry').collect do |service| 
        OpenStruct.new(
          #:label => Services[service.at("service").inner_html],
          :service_code => service.at("service").inner_html,
          :cost => service.at("estimatedcharges/discountedcharges/netcharge").inner_html.to_f
          ) #||  service.at("transportationcharges/monetaryvalue").inner_html.to_f)
      end.sort_by(&:cost)
    end

  	Services = {
			"priority" => "PRIORITYOVERNIGHT",
			"2day" => "FEDEX2DAY",
			"standard_overnight" => "STANDARDOVERNIGHT",
			"first_overnight" => "FIRSTOVERNIGHT",
			"express_saver" => "FEDEXEXPRESSSAVER",
			"1day_freight" => "FEDEX1DAYFREIGHT",
			"2day_freight" => "FEDEX2DAYFREIGHT",
			"3day_freight" => "FEDEX3DAYFREIGHT",
			"international_priority" => "INTERNATIONALPRIORITY",
			"international_economy" => "INTERNATIONALECONOMY",
			"international_first" => "INTERNATIONALFIRST",
			"international_priority_freight" => "INTERNATIONALPRIORITYFREIGHT",
			"international_economy_freight" => "INTERNATIONALECONOMYFREIGHT",
			"home_delivery" => "GROUNDHOMEDELIVERY",
			"ground_service" => "FEDEXGROUND",
			"international_ground_service" => "INTERNATIONALGROUND"
		}


    protected

      def send_request(data)
        uri = URI.parse(@prefs[:url])
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
        @logger.info "Sending to FedEx:\n #{data}" if @prefs[:debug]
        body = http.post(uri.request_uri, data).body
        @logger.info "Received from FedEx:\n #{body}" if @prefs[:debug]
        if @error =  Hpricot(body).at('error').inner_html rescue nil
          raise FedExError, @error
        end
        body
      end

      def rate_request(from_zip, destination, weight, boxes, home_delivery = false)
        xml = Builder::XmlMarkup.new :indent => 2
        xml.instruct!
        xml.FDXRateAvailableServicesRequest('xmlns:api' => 'http://www.fedex.com/fsmapi', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:noNamespaceSchemaLocation' => 'FDXRateAvailableServicesRequest.xsd') do
          xml.RequestHeader do
            xml.AccountNumber @prefs[:account]
            xml.MeterNumber @prefs[:meter]
            #xml.CarrierCode 'ANY'
          end
          #xml.Service 'FEDEX2DAY'
          #xml.ShipDate '2008-10-30'
          xml.ReturnShipmentIndicator 'NONRETURN'
          xml.DropoffType 'REGULARPICKUP'
          xml.Packaging 'YOURPACKAGING'
          xml.WeightUnits 'LBS'
				  xml.Weight weight
				  xml.ListRate true #tells fedex to return list rates as well as discounted rates
				  xml.OriginAddress do
					 xml.StateOrProvinceCode 'CA'
					 xml.PostalCode from_zip
					 xml.CountryCode 'US'
				  end
				  if home_delivery == true
				    xml.HomeDelivery do
              xml.Type 'APPOINTMENT'
			      end
			    end
				  xml.DestinationAddress do
					  xml.StateOrProvinceCode self.state_from_zip(destination)
					  xml.PostalCode destination
					  xml.CountryCode "US"
  				end
  				xml.Payment do
  				  xml.PayorType 'SENDER'
				  end
  				xml.PackageCount boxes
        end
      self.send_request(xml.target!)
      end

    	def state_from_zip(zip)
			zip = zip.to_i
			{
				(99500...99929) => "AK", 
				(35000...36999) => "AL", 
				(71600...72999) => "AR", 
				(75502...75505) => "AR", 
				(85000...86599) => "AZ", 
				(90000...96199) => "CA", 
				(80000...81699) => "CO", 
				(6000...6999) => "CT", 
				(20000...20099) => "DC", 
				(20200...20599) => "DC", 
				(19700...19999) => "DE", 
				(32000...33999) => "FL", 
				(34100...34999) => "FL", 
				(30000...31999) => "GA", 
				(96700...96798) => "HI", 
				(96800...96899) => "HI", 
				(50000...52999) => "IA", 
				(83200...83899) => "ID", 
				(60000...62999) => "IL", 
				(46000...47999) => "IN", 
				(66000...67999) => "KS", 
				(40000...42799) => "KY", 
				(45275...45275) => "KY", 
				(70000...71499) => "LA", 
				(71749...71749) => "LA", 
				(1000...2799) => "MA", 
				(20331...20331) => "MD", 
				(20600...21999) => "MD", 
				(3801...3801) => "ME", 
				(3804...3804) => "ME", 
				(3900...4999) => "ME", 
				(48000...49999) => "MI", 
				(55000...56799) => "MN", 
				(63000...65899) => "MO", 
				(38600...39799) => "MS", 
				(59000...59999) => "MT", 
				(27000...28999) => "NC", 
				(58000...58899) => "ND", 
				(68000...69399) => "NE", 
				(3000...3803) => "NH", 
				(3809...3899) => "NH", 
				(7000...8999) => "NJ", 
				(87000...88499) => "NM", 
				(89000...89899) => "NV", 
				(400...599) => "NY", 
				(6390...6390) => "NY", 
				(9000...14999) => "NY", 
				(43000...45999) => "OH", 
				(73000...73199) => "OK", 
				(73400...74999) => "OK", 
				(97000...97999) => "OR", 
				(15000...19699) => "PA", 
				(2800...2999) => "RI", 
				(6379...6379) => "RI", 
				(29000...29999) => "SC", 
				(57000...57799) => "SD", 
				(37000...38599) => "TN", 
				(72395...72395) => "TN", 
				(73300...73399) => "TX", 
				(73949...73949) => "TX", 
				(75000...79999) => "TX", 
				(88501...88599) => "TX", 
				(84000...84799) => "UT", 
				(20105...20199) => "VA", 
				(20301...20301) => "VA", 
				(20370...20370) => "VA", 
				(22000...24699) => "VA", 
				(5000...5999) => "VT", 
				(98000...99499) => "WA", 
				(49936...49936) => "WI", 
				(53000...54999) => "WI", 
				(24700...26899) => "WV", 
				(82000...83199) => "WY"
				}.each do |range, state|
					return state if range.include? zip
				end

				raise ShippingError, "Invalid zip code"
			end

  end
end