require 'active_resource'
require 'active_support/core_ext/class/attribute_accessors'
require 'digest/md5'

module ShopifyAPI
  METAFIELD_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article Variant)
  EVENT_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article )
  
  module Countable
    def count(options = {})
      Integer(get(:count, options))
    end
  end
  
  module Metafields
    def metafields
      Metafield.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
    end
    
    def add_metafield(metafield)
      raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
      
      metafield.prefix_options = {
        :resource => self.class.collection_name,
        :resource_id => id
      }
      metafield.save
      metafield
    end
  end

  module Events
    def events
      Event.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
    end
  end
    
  # 
  #  The Shopify API authenticates each call via HTTP Authentication, using
  #    * the application's API key as the username, and
  #    * a hex digest of the application's shared secret and an 
  #      authentication token as the password.
  #  
  #  Generation & acquisition of the beforementioned looks like this:
  # 
  #    0. Developer (that's you) registers Application (and provides a
  #       callback url) and receives an API key and a shared secret
  # 
  #    1. User visits Application and are told they need to authenticate the
  #       application first for read/write permission to their data (needs to
  #       happen only once). User is asked for their shop url.
  # 
  #    2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?api_key=<API key>
  #       (See Session#create_permission_url)
  # 
  #    3. User logs-in to Shopify, approves application permission request
  # 
  #    4. Shopify redirects to the Application's callback url (provided during
  #       registration), including the shop's name, and an authentication token in the parameters:
  #         GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
  # 
  #    5. Authentication password computed using the shared secret and the
  #       authentication token (see Session#computed_password)
  # 
  #    6. Profit!
  #       (API calls can now authenticate through HTTP using the API key, and
  #       computed password)
  # 
  #  LoginController and ShopifyLoginProtection use the Session class to set Shopify::Base.site
  #  so that all API calls are authorized transparently and end up just looking like this:
  # 
  #    # get 3 products
  #    @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
  #    
  #    # get latest 3 orders
  #    @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
  # 
  #  As an example of what your LoginController should look like, take a look
  #  at the following:
  # 
  #    class LoginController < ApplicationController
  #      def index
  #        # Ask user for their #{shop}.myshopify.com address
  #      end
  #    
  #      def authenticate
  #        redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
  #      end
  #    
  #      # Shopify redirects the logged-in user back to this action along with
  #      # the authorization token t.
  #      # 
  #      # This token is later combined with the developer's shared secret to form
  #      # the password used to call API methods.
  #      def finalize
  #        shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
  #        if shopify_session.valid?
  #          session[:shopify] = shopify_session
  #          flash[:notice] = "Logged in to shopify store."
  #    
  #          return_address = session[:return_to] || '/home'
  #          session[:return_to] = nil
  #          redirect_to return_address
  #        else
  #          flash[:error] = "Could not log in to Shopify store."
  #          redirect_to :action => 'index'
  #        end
  #      end
  #    
  #      def logout
  #        session[:shopify] = nil
  #        flash[:notice] = "Successfully logged out."
  #    
  #        redirect_to :action => 'index'
  #      end
  #    end
  # 
  class Session
    cattr_accessor :api_key
    cattr_accessor :secret
    cattr_accessor :protocol 
    self.protocol = 'https'

    attr_accessor :url, :token, :name
    
    def self.setup(params)
      params.each { |k,value| send("#{k}=", value) }
    end

    def initialize(url, token = nil, params = nil)
      self.url, self.token = url, token

      if params
        unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
          raise "Invalid Signature: Possible malicious login" 
        end
      end

      self.class.prepare_url(self.url)
    end
    
    def shop
      Shop.current
    end
    
    def create_permission_url
      return nil if url.blank? || api_key.blank?
      "http://#{url}/admin/api/auth?api_key=#{api_key}"
    end

    # Used by ActiveResource::Base to make all non-authentication API calls
    # 
    # (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
    def site
      "#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
    end

    def valid?
      url.present? && token.present?
    end

    private

    # The secret is computed by taking the shared_secret which we got when 
    # registring this third party application and concating the request_to it, 
    # and then calculating a MD5 hexdigest. 
    def computed_password
      Digest::MD5.hexdigest(secret + token.to_s)
    end
    
    def self.prepare_url(url)
      return nil if url.blank?
      url.gsub!(/https?:\/\//, '')                            # remove http:// or https://
      url.concat(".myshopify.com") unless url.include?('.')   # extend url to myshopify.com if no host is given
    end
    
    def self.validate_signature(params)
      return false unless signature = params[:signature]

      sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
      Digest::MD5.hexdigest(secret + sorted_params) == signature
    end
  end
  
  class Base < ActiveResource::Base
    extend Countable
  end

  # Shop object. Use Shop.current to receive 
  # the shop.
  class Shop < Base
    def self.current
      find(:one, :from => "/admin/shop.#{format.extension}")
    end
    
    def metafields
      Metafield.find(:all)
    end
    
    def add_metafield(metafield)
      raise ArgumentError, "You can only add metafields to resource that has been saved" if new?      
      metafield.save
      metafield
    end
    
    def events
      Event.find(:all)
    end
  end               

  # Custom collection
  #
  class CustomCollection < Base
    def products
      Product.find(:all, :params => {:collection_id => self.id})
    end
    
    def add_product(product)
      Collect.create(:collection_id => self.id, :product_id => product.id)
    end
    
    def remove_product(product)
      collect = Collect.find(:first, :params => {:collection_id => self.id, :product_id => product.id})
      collect.destroy if collect
    end
  end                                                                 
  
  class SmartCollection < Base
    def products
      Product.find(:all, :params => {:collection_id => self.id})
    end
  end  
  
  # For adding/removing products from custom collections
  class Collect < Base
  end

  class ShippingAddress < Base
  end

  class BillingAddress < Base
  end         

  class LineItem < Base 
  end       

  class ShippingLine < Base
  end  

  class NoteAttribute < Base
  end

  class Order < Base
    def close; load_attributes_from_response(post(:close)); end

    def open; load_attributes_from_response(post(:open)); end

    def transactions
      Transaction.find(:all, :params => { :order_id => id })
    end
    
    def capture(amount = "")
      Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
    end
  end
  
  class Product < Base

    # Share all items of this store with the 
    # shopify marketplace
    def self.share; post :share;  end    
    def self.unshare; delete :share; end

    # compute the price range
    def price_range
      prices = variants.collect(&:price)
      format =  "%0.2f"
      if prices.min != prices.max
        "#{format % prices.min} - #{format % prices.max}"
      else
        format % prices.min
      end
    end
    
    def collections
      CustomCollection.find(:all, :params => {:product_id => self.id})
    end
    
    def smart_collections
      SmartCollection.find(:all, :params => {:product_id => self.id})
    end
    
    def add_to_collection(collection)
      collection.add_product(self)
    end
    
    def remove_from_collection(collection)
      collection.remove_product(self)
    end
  end
  
  class Variant < Base
    self.prefix = "/admin/products/:product_id/"
  end
  
  class Image < Base
    self.prefix = "/admin/products/:product_id/"
    
    # generate a method for each possible image variant
    [:pico, :icon, :thumb, :small, :compact, :medium, :large, :original].each do |m|
      reg_exp_match = "/\\1_#{m}.\\2"
      define_method(m) { src.gsub(/\/(.*)\.(\w{2,4})/, reg_exp_match) }
    end
    
    def attach_image(data, filename = nil)
      attributes['attachment'] = Base64.encode64(data)
      attributes['filename'] = filename unless filename.nil?
    end
  end

  class Transaction < Base
    self.prefix = "/admin/orders/:order_id/"
  end
  
  class Fulfillment < Base
    self.prefix = "/admin/orders/:order_id/"
  end

  class Country < Base
  end

  class Page < Base
  end
  
  class Blog < Base
    def articles
      Article.find(:all, :params => { :blog_id => id })
    end
  end
  
  class Article < Base
    self.prefix = "/admin/blogs/:blog_id/"
  end
  
  class Metafield < Base
    self.prefix = "/admin/:resource/:resource_id/"
    
    # Hack to allow both Shop and other Metafields in through the same AR class
    def self.prefix(options={})
      options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
    end
            
    def value
      return if attributes["value"].nil?
      attributes["value_type"] == "integer" ? attributes["value"].to_i : attributes["value"]
    end

  end

  class Comment < Base 
    def remove; load_attributes_from_response(post(:remove)); end
    def ham; load_attributes_from_response(post(:ham)); end
    def spam; load_attributes_from_response(post(:spam)); end
    def approve; load_attributes_from_response(post(:approve)); end        
  end
  
  class Province < Base
    self.prefix = "/admin/countries/:country_id/"
  end
  
  class Redirect < Base
  end
  
  class Webhook < Base
  end
  
  class Event < Base
    self.prefix = "/admin/:resource/:resource_id/"
    
    # Hack to allow both Shop and other Events in through the same AR class
    def self.prefix(options={})
      options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
    end
  end
  
  # Assets represent the files that comprise your theme.
  # There are different buckets which hold different kinds
  # of assets, each corresponding to one of the folders
  # within a theme's zip file: layout, templates, and
  # assets. The full key of an asset always starts with the
  # bucket name, and the path separator is a forward slash,
  # like layout/theme.liquid or assets/bg-body.gif.
  #
  # Initialize with a key:
  #   asset = ShopifyAPI::Asset.new(:key => 'assets/special.css')
  # 
  # Find by key:
  #   asset = ShopifyAPI::Asset.find('assets/image.png')
  # 
  # Get the text or binary value:
  #   asset.value # decodes from attachment attribute if necessary
  # 
  # You can provide new data for assets in a few different ways:
  # 
  #   * assign text data for the value directly:
  #       asset.value = "div.special {color:red;}"
  #     
  #   * provide binary data for the value:
  #       asset.attach(File.read('image.png'))
  #     
  #   * set a URL from which Shopify will fetch the value:
  #       asset.src = "http://mysite.com/image.png"
  #     
  #   * set a source key of another of your assets from which
  #     the value will be copied:
  #       asset.source_key = "assets/another_image.png"
  class Asset < Base
    self.primary_key = 'key'
    
    # find an asset by key:
    #   ShopifyAPI::Asset.find('layout/theme.liquid')
    def self.find(*args)
      if args[0].is_a?(Symbol)
        super
      else
        find(:one, :from => "/admin/assets.#{format.extension}", :params => {:asset => {:key => args[0]}})
      end
    end
    
    # For text assets, Shopify returns the data in the 'value' attribute.
    # For binary assets, the data is base-64-encoded and returned in the
    # 'attachment' attribute. This accessor returns the data in both cases.
    def value
      attributes['value'] ||
      (attributes['attachment'] ? Base64.decode64(attributes['attachment']) : nil)
    end
    
    def attach(data)
      self.attachment = Base64.encode64(data)
    end
    
    def destroy #:nodoc:
      connection.delete(element_path(:asset => {:key => key}), self.class.headers)
    end
    
    def new? #:nodoc:
      false
    end
    
    def self.element_path(id, prefix_options = {}, query_options = nil) #:nodoc:
      prefix_options, query_options = split_options(prefix_options) if query_options.nil?
      "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
    end
    
    def method_missing(method_symbol, *arguments) #:nodoc:
      if %w{value= attachment= src= source_key=}.include?(method_symbol)
        wipe_value_attributes
      end
      super
    end
    
    private
    
    def wipe_value_attributes
      %w{value attachment src source_key}.each do |attr|
        attributes.delete(attr)
      end
    end
  end
  
  class RecurringApplicationCharge < Base
    undef_method :test

    def self.current
      find(:all).find{|charge| charge.status == 'active'}
    end
    
    def cancel
      load_attributes_from_response(self.destroy)
    end
    
    def activate
      load_attributes_from_response(post(:activate))
    end
  end

  class ApplicationCharge < Base
    undef_method :test

    def activate
      load_attributes_from_response(post(:activate))
    end
  end

  class ProductSearchEngine < Base
  end
  
  # Include Metafields module in all enabled classes
  METAFIELD_ENABLED_CLASSES.each do |klass|
    "ShopifyAPI::#{klass}".constantize.send(:include, Metafields)
  end
  
  # Include Events module in all enabled classes
  EVENT_ENABLED_CLASSES.each do |klass|
    "ShopifyAPI::#{klass}".constantize.send(:include, Events)
  end
end