require 'xmlrpc/client'

class HominidError < RuntimeError
  def initialize(message)
    super(message)
  end
end

class HominidCommunicationError < HominidError
  def initialize(message)
    super(message)
  end
end

class Hominid
  
  # MailChimp API Documentation: http://www.mailchimp.com/api/1.2/
  MAILCHIMP_API = "http://api.mailchimp.com/1.2/"

  def initialize(config = {})
    if defined?(RAILS_ROOT) && (!config || config.empty?)
      config = YAML.load(File.open("#{RAILS_ROOT}/config/hominid.yml"))[RAILS_ENV].symbolize_keys
    end
    config.merge(:username => config[:username].to_s, :password => config[:password].to_s)
    defaults = {:send_welcome => false, :double_opt_in => false, :update_existing => true, :replace_interests => true, :user_info => {}}
    @config = defaults.merge(config).freeze
    @chimpApi = XMLRPC::Client.new2(MAILCHIMP_API)
  end

  ## Security related methods
  
  def add_api_key
    @chimpApi.call("apikeyAdd", *@config.values_at(:username, :password, :api_key))
  end
  
  def expire_api_key
    @chimpApi.call("apikeyExpire", *@config.values_at(:username, :password, :api_key))
  end
  
  def api_keys(include_expired = false)
    username, password = *@config.values_at(:username, :password)
    @chimpApi.call("apikeys", username, password, include_expired)
  end
  
  ## Campaign related methods
  
  def campaign_stats(campaign_id)
    # Get the stats of a campaign
    call("campaignStats", campaign_id)
  end
  
  def campaign_content(campaign_id, for_archive = true)
    # Get the content of a campaign
    #
    # for_archive controls whether we return the Archive version (true) or the Raw version (false), defaults to true
    call("campaignContent", campaign_id, for_archive)
  end
  
  def campaigns(filters = {}, start = 0, limit = 50)
    # Get the campaigns for this account
    # API Version 1.2 requires that filters be sent as a hash
    # Available options for the filters hash are:
    #
    #   :campaign_id    = (string)  The ID of the campaign you wish to return. 
    #   :list_id        = (string)  Show only campaigns with this list_id. 
    #   :folder_id      = (integer) Show only campaigns from this folder.
    #   :from_name      = (string)  Show only campaigns with this from_name.
    #   :from_email     = (string)  Show only campaigns with this from_email.
    #   :title          = (string)  Show only campaigns with this title.
    #   :subject        = (string)  Show only campaigns with this subject.
    #   :sedtime_start  = (string)  Show campaigns sent after YYYY-MM-DD HH:mm:ss.
    #   :sendtime_end   = (string)  Show campaigns sent before YYYY-MM-DD HH:mm:ss.
    #   :subject        = (boolean) Filter by exact values, or search within content for filter values.
    call("campaigns", filters, start, limit)
  end

  # Attach Ecommerce Order Information to a Campaign.
  # The order hash should be structured as follows: 
  #
  #   :id             = (string)  the order id 
  #   :campaign_id    = (string)  the campaign id to track the order (mc_cid query string). 
  #   :email_id       = (string)  email id of the subscriber (mc_eid query string)
  #   :total          = (double)  Show only campaigns with this from_name.
  #   :shipping       = (string)  *optional - the total paid for shipping fees.
  #   :tax            = (string)  *optional - the total tax paid.
  #   :store_id       = (string)  a unique id for the store sending the order in
  #   :store_name     = (string)  *optional - A readable name for the store, typicaly the hostname.
  #   :plugin_id      = (string)  the MailChimp-assigned Plugin Id. Using 1214 for the moment. 
  #   :items          = (array)   the individual line items for an order, using the following keys:
  #     
  #     :line_num      = (integer) *optional - line number of the item on the order
  #     :product_id    = (integer) internal product id
  #     :product_name  = (string)  the name for the product_id associated with the item
  #     :category_id   = (integer) internal id for the (main) category associated with product
  #     :category_name = (string)  the category name for the category id
  #     :qty           = (double)  the quantity of items ordered
  #     :cost          = (double)  the cost of a single item (i.e., not the extended cost of the line)
  def campaign_ecomm_add_order(order)
    call("campaignEcommAddOrder", order)
  end

  def create_campaign(type = 'regular', options = {}, content = {}, segment_options = {}, type_opts = {})
    # Create a new campaign
    call("campaignCreate", type, options, content, segment_options, type_opts)
  end
  
  def delete_campaign(campaign_id)
    # Delete a campaign
    call("campaignDelete", campaign_id)
  end
  
  def replicate_campaign(campaign_id)
    # Replicate a campaign (returns ID of new campaign)
    call("campaignReplicate", campaign_id)
  end
  
  def schedule_campaign(campaign_id, time = "#{1.day.from_now}")
    # Schedule a campaign
    ## TODO: Add support for A/B Split scheduling
    call("campaignSchedule", campaign_id, time)
  end
  
  def send_now(campaign_id)
    # Send a campaign
    call("campaignSendNow", campaign_id)
  end
  
  def send_test(campaign_id, emails = {})
    # Send a test of a campaign
    call("campaignSendTest", campaign_id, emails)
  end
  
  def templates
    # Get the templates
    call("campaignTemplates")
  end
  
  def update_campaign(campaign_id, name, value)
    # Update a campaign
    call("campaignUpdate", campaign_id, name, value)
  end
  
  def unschedule_campaign(campaign_id)
    # Unschedule a campaign
    call("campaignUnschedule", campaign_id)
  end
  
  ## Helper methods
  
  def html_to_text(content)
    # Convert HTML content to text
    call("generateText", 'html', content)
  end
  
  def convert_css_to_inline(html, strip_css = false)
    # Convert CSS styles to inline styles and (optionally) remove original styles
    call("inlineCss", html, strip_css)
  end
  
  ## List related methods
  
  def lists
    # Get all of the lists for this mailchimp account
    call("lists")
  end
  
  def create_group(list_id, group)
    # Add an interest group to a list
    call("listInterestGroupAdd", list_id, group)
  end
  
  def create_tag(list_id, tag, name, required = false)
    # Add a merge tag to a list
    call("listMergeVarAdd", list_id, tag, name, required)
  end
  
  def delete_group(list_id, group)
    # Delete an interest group for a list
    call("listInterestGroupDel", list_id, group)
  end
  
  def delete_tag(list_id, tag)
    # Delete a merge tag and all its members
    call("listMergeVarDel", list_id, tag)
  end
  
  def groups(list_id)
    # Get the interest groups for a list
    call("listInterestGroups", list_id)
  end
  
  def member(list_id, email)
    # Get a member of a list
    call("listMemberInfo", list_id, email)
  end
  
  def members(list_id, status = "subscribed", since = "2000-01-01 00:00:00", start = 0, limit = 100)
    # Get members of a list based on status
    # Select members based on one of the following statuses:
    #   'subscribed'
    #   'unsubscribed'
    #   'cleaned'
    #   'updated'
    #
    # Select members that have updated their status or profile by providing
    # a "since" date in the format of YYYY-MM-DD HH:MM:SS
    # 
    call("listMembers", list_id, status, since, start, limit)
  end
  
  def merge_tags(list_id)
    # Get the merge tags for a list
    call("listMergeVars", list_id)
  end
  
  def subscribe(list_id, email, options = {})
    options = apply_defaults_to({:email_type => "html"}.merge(options))
    # Subscribe a member
    call("listSubscribe", list_id, email, *options.values_at(:user_info, :email_type, :double_opt_in, :update_existing, :replace_interests, :send_welcome))
  end
  
  def subscribe_many(list_id, subscribers, options = {})
    options = apply_defaults_to({:update_existing => true}.merge(options))
    # Subscribe a batch of members
    # subscribers = {:EMAIL => 'example@email.com', :EMAIL_TYPE => 'html'} 
    call("listBatchSubscribe", list_id, subscribers, *options.values_at(:double_opt_in, :update_existing, :replace_interests))
  end
  
  # Unsubscribe a user from a list
  #
  # Options
  # :delete_member    Delete user form list instead of marking them as unsubscribed
  # :send_goodbye     Send goodbye e-mail to user
  # :send_notify      Send unsubscribe email to email in list settings
  def unsubscribe(list_id, current_email, options = {})
    options = apply_defaults_to({:delete_member => true}.merge(options))
    # Unsubscribe a list member
    call("listUnsubscribe", list_id, current_email, *options.values_at(:delete_member, :send_goodbye, :send_notify))
  end
  
  def unsubscribe_many(list_id, emails, options = {})
    options = apply_defaults_to({:delete_member => true}.merge(options))
    # Unsubscribe an array of email addresses
    # emails = ['first@email.com', 'second@email.com'] 
    call("listBatchUnsubscribe", list_id, emails, *options.values_at(:delete_member, :send_goodbye, :send_notify))
  end
  
  def update_member(list_id, current_email, user_info = {}, email_type = "")
    # Update a member of this list
    call("listUpdateMember", list_id, current_email, user_info, email_type, true)
  end
  
  protected
  def apply_defaults_to(options)
    @config.merge(options)
  end

  def call(method, *args)
    @chimpApi.call(method, @config[:api_key], *args)
  rescue XMLRPC::FaultException => error
    raise HominidError.new(error.message)
  rescue Exception => error
    raise HominidCommunicationError.new(error.message)
  end
end