class JamRuby::AffiliatePartner < ActiveRecord::Base
  self.table_name = 'affiliate_partners'

  belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner
  has_one :school, class_name: "JamRuby::School"
  has_one :retailer, class_name: "JamRuby::Retailer"
  has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id
  belongs_to :affiliate_legalese, :class_name => "JamRuby::AffiliateLegalese", :foreign_key => :legalese_id
  has_many :sale_line_items, :class_name => 'JamRuby::SaleLineItem', foreign_key: :affiliate_referral_id
  has_many :quarters, :class_name => 'JamRuby::AffiliateQuarterlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
  has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
  has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
  has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
  has_many :affiliate_distributions, :class_name => "JamRuby::AffiliateDistribution", foreign_key: :affiliate_referral_id
  has_many :links, :class_name => "JamRuby::AffiliateLink", foreign_key: :affiliate_partner_id
  attr_accessible :partner_name, :partner_code, :partner_user_id, :entity_type, :rate, as: :admin

  ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other }

  KEY_ADDR1 = 'address1'
  KEY_ADDR2 = 'address2'
  KEY_CITY = 'city'
  KEY_STATE = 'state'
  KEY_POSTAL = 'postal_code'
  KEY_COUNTRY = 'country'

  GUITAR_CENTER = 'guitar_center'

  # ten dollars in cents
  PAY_THRESHOLD = 10 * 100

  AFFILIATE_PARAMS="utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate="

  ADDRESS_SCHEMA = {
      KEY_ADDR1 => '',
      KEY_ADDR2 => '',
      KEY_CITY => '',
      KEY_STATE => '',
      KEY_POSTAL => '',
      KEY_COUNTRY => '',
  }

  PARAM_REFERRAL = :ref
  PARAM_COOKIE = :affiliate_ref

  PARTNER_CODE_REGEX = /^[#{Regexp.escape('abcdefghijklmnopqrstuvwxyz0123456789-._+,')}]+{2,128}$/i

  #validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email
  #validates :partner_code, format: { with: PARTNER_CODE_REGEX }, :allow_blank => true
  validates :entity_type, inclusion: {in: ENTITY_TYPES, message: "invalid entity type"}

  #serialize :address, JSON

  before_save do |record|
    record.address ||= ADDRESS_SCHEMA.clone
    record.entity_type ||= ENTITY_TYPES.first
    record.partner_user_id = nil if record.partner_user_id == '' #for activeadmin coercion
  end

  def display_name
    partner_name || (partner_user ? partner_user.name : 'abandoned')
  end

  def admin_url
    APP_CONFIG.admin_root_url + "/admin/affiliates/#{id}"
  end

  # used by admin
  def self.create_with_params(params={})
    raise 'not supported'
    oo = AffiliatePartner.new
    oo.partner_name = params[:partner_name].try(:strip)
    oo.partner_code = params[:partner_code].try(:strip).try(:downcase)
    oo.partner_user = User.where(:email => params[:user_email].try(:strip)).limit(1).first
    oo.partner_user_id = oo.partner_user.try(:id)
    oo.entity_type = params[:entity_type] || ENTITY_TYPES.first
    oo.save
    oo
  end

  # used by web
  def self.create_with_web_params(user, params={})
    oo = AffiliatePartner.new
    oo.partner_name = params[:partner_name].try(:strip)
    oo.partner_user = user if user # user is not required
    oo.entity_type = params[:entity_type] || ENTITY_TYPES.first
    oo.signed_at = Time.now
    oo.save
    oo
  end

  def self.create_from_school(school)
    oo = AffiliatePartner.new
    oo.partner_name = "Affiliate from School #{school.id}"
    oo.partner_user = school.owner
    oo.entity_type = 'Other'
    oo.school = school
    oo.signed_at = nil
    oo.save
  end

  def self.create_from_retailer(retailer)
    oo = AffiliatePartner.new
    oo.partner_name = "Affiliate from Retailer #{retailer.id}"
    oo.partner_user = retailer.owner
    oo.entity_type = 'Other'
    oo.retailer = retailer
    oo.signed_at = nil
    oo.save
  end


  def self.coded_id(code=nil)
    self.where(:partner_code => code).limit(1).pluck(:id).first if code.present?
  end

  def self.is_code?(code)
    self.where(:partner_code => code).limit(1).pluck(:id).present?
  end

  def referrals_by_date
    by_date = User.where(:affiliate_referral_id => self.id)
                  .group('DATE(created_at)')
                  .having("COUNT(*) > 0")
                  .order('date_created_at DESC')
                  .count
    block_given? ? yield(by_date) : by_date
  end

  def signed_legalese(legalese)
    self.affiliate_legalese = legalese
    self.signed_at = Time.now
    save!
  end

  def update_address_value(key, val)
    self.address[key] = val
    self.update_attribute(:address, self.address)
  end

  def address_value(key)
    self.address[key]
  end

  def created_within_affiliate_window(user, sale_time)
    sale_time - user.created_at < 2.years
  end

  def should_attribute_sale?(shopping_cart, user_to_check, instance)

    if created_within_affiliate_window(user_to_check, Time.now)
      product_info = shopping_cart.product_info(instance)
      # subtract the total quantity from the freebie quantity, to see how much we should attribute to them
      real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i

      if shopping_cart.is_lesson?
        applicable_rate = lesson_rate
      else
        applicable_rate = rate
      end
      {fee_in_cents: (product_info[:price] * 100 * real_quantity * applicable_rate.to_f).round}
    else
      false
    end
  end

  def cumulative_earnings_in_dollars
    cumulative_earnings_in_cents.to_f / 100.to_f
  end

  def self.quarter_info(date)

    year = date.year

    # which quarter?
    quarter = -1
    if date.month >= 1 && date.month <= 3
      quarter = 0
    elsif date.month >= 4 && date.month <= 6
      quarter = 1
    elsif date.month >= 7 && date.month <= 9
      quarter = 2
    elsif date.month >= 10 && date.month <= 12
      quarter = 3
    end

    raise 'quarter should never be -1' if quarter == -1

    previous_quarter = quarter - 1
    previous_year = date.year
    if previous_quarter == -1
      previous_quarter = 3
      previous_year = year - 1
    end

    raise 'previous quarter should never be -1' if previous_quarter == -1

    {year: year, quarter: quarter, previous_quarter: previous_quarter, previous_year: previous_year}
  end

  def self.did_quarter_elapse?(quarter_info, last_tallied_info)
    if last_tallied_info.nil?
      true
    else
      quarter_info == last_tallied_info
    end
  end

  # meant to be run regularly; this routine will make summarized counts in the
  # AffiliateQuarterlyPayment table
  # AffiliatePartner.cumulative_earnings_in_cents, AffiliatePartner.referral_user_count
  def self.tally_up(day)

    AffiliatePartner.transaction do
      quarter_info = quarter_info(day)
      last_tallied_info = quarter_info(GenericState.affiliate_tallied_at) if GenericState.affiliate_tallied_at

      quarter_elapsed = did_quarter_elapse?(quarter_info, last_tallied_info)

      if quarter_elapsed
        tally_monthly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter])
        tally_quarterly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter])
      end
      tally_monthly_payments(quarter_info[:year], quarter_info[:quarter])
      tally_quarterly_payments(quarter_info[:year], quarter_info[:quarter])

      tally_traffic_totals(GenericState.affiliate_tallied_at, day)

      tally_partner_totals

      state = GenericState.singleton
      state.affiliate_tallied_at = day
      state.save!
    end

  end

  # this just makes sure that the quarter rows exist before later manipulations with UPDATEs
  def self.ensure_quarters_exist(year, quarter)

    sql = %{
    INSERT INTO affiliate_quarterly_payments (quarter, year, affiliate_partner_id)
    (SELECT #{quarter}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN
      (SELECT affiliate_partner_id FROM affiliate_quarterly_payments WHERE year = #{year} AND quarter = #{quarter}))
    }

    ActiveRecord::Base.connection.execute(sql)
  end

  # this just makes sure that the quarter rows exist before later manipulations with UPDATEs
  def self.ensure_months_exist(year, quarter)

    months = [1, 2, 3].collect! { |i| quarter * 3 + i  }

    months.each do |month|
      sql = %{
        INSERT INTO affiliate_monthly_payments (month, year, affiliate_partner_id)
        (SELECT #{month}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN
          (SELECT affiliate_partner_id FROM affiliate_monthly_payments WHERE year = #{year} AND month = #{month}))
      }

      ActiveRecord::Base.connection.execute(sql)
    end
  end


  def self.sale_items_subquery(start_date, end_date, table_name)
    %{
    FROM affiliate_distributions inner join sale_line_items ON affiliate_distributions.sale_line_item_id = sale_line_items.id
    WHERE
      (DATE(affiliate_distributions.created_at) >= DATE('#{start_date}') AND DATE(affiliate_distributions.created_at) <= DATE('#{end_date}'))
        AND
      affiliate_distributions.affiliate_referral_id = #{table_name}.affiliate_partner_id
    }
  end

  def self.sale_items_refunded_subquery(start_date, end_date, table_name)
    %{
    FROM affiliate_distributions inner join sale_line_items ON affiliate_distributions.sale_line_item_id = sale_line_items.id
    WHERE
      (DATE(affiliate_distributions.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(affiliate_distributions.affiliate_refunded_at) <= DATE('#{end_date}'))
        AND
      affiliate_distributions.affiliate_referral_id = #{table_name}.affiliate_partner_id
        AND
      affiliate_distributions.affiliate_refunded = TRUE
    }
  end
  # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id
  # don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE
  def self.total_months(year, quarter)

    months = [1, 2, 3].collect! { |i| quarter * 3 + i  }


    months.each do |month|

      start_date, end_date = boundary_dates_for_month(year, month)

      sql = %{
      UPDATE affiliate_monthly_payments
        SET
          last_updated = NOW(),
          jamtracks_sold =
            COALESCE(
              (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
              #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')}
            ), 0)
            +
            COALESCE(
              (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
              #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')}
            ), 0),
          due_amount_in_cents =
            COALESCE(
              (SELECT SUM(affiliate_distributions.affiliate_referral_fee_in_cents)
              #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')}
            ), 0)
            +
            COALESCE(
              (SELECT -SUM(affiliate_distributions.affiliate_referral_fee_in_cents)
              #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')}
            ), 0)

      WHERE closed = FALSE AND year = #{year} AND month = #{month}
      }

      ActiveRecord::Base.connection.execute(sql)
    end
  end

  # close any quarters that are done, so we don't manipulate them again
  def self.close_months(year, quarter)
    # close any quarters that occurred before this quarter
    month = quarter * 3 + 1

    sql = %{
    UPDATE affiliate_monthly_payments
      SET
        closed = TRUE, closed_at = NOW()
        WHERE year < #{year} OR month < #{month}
    }

    ActiveRecord::Base.connection.execute(sql)

  end

  # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id
  # don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE
  def self.total_quarters(year, quarter)
    start_date, end_date = boundary_dates(year, quarter)

    sql = %{
      UPDATE affiliate_quarterly_payments
        SET
          last_updated = NOW(),
          jamtracks_sold =
            COALESCE(
              (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
              #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
            ), 0)
            +
            COALESCE(
              (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
              #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
            ), 0),
          due_amount_in_cents =
            COALESCE(
              (SELECT SUM(affiliate_distributions.affiliate_referral_fee_in_cents)
              #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
            ), 0)
            +
            COALESCE(
              (SELECT -SUM(affiliate_distributions.affiliate_referral_fee_in_cents)
              #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
            ), 0)

      WHERE closed = FALSE AND paid = FALSE AND year = #{year} AND quarter = #{quarter}
    }

    ActiveRecord::Base.connection.execute(sql)
  end

  # close any quarters that are done, so we don't manipulate them again
  def self.close_quarters(year, quarter)
    # close any quarters that occurred before this quarter
    sql = %{
      UPDATE affiliate_quarterly_payments
        SET
          closed = TRUE, closed_at = NOW()
          WHERE year < #{year} OR (year = #{year} AND quarter < #{quarter})
    }

    ActiveRecord::Base.connection.execute(sql)
  end

  def self.tally_quarterly_payments(year, quarter)
    ensure_quarters_exist(year, quarter)

    total_quarters(year, quarter)

    close_quarters(year, quarter)
  end


  def self.tally_monthly_payments(year, quarter)
    ensure_months_exist(year, quarter)

    total_months(year, quarter)

    close_months(year, quarter)
  end

  def self.tally_partner_totals
    sql = %{
      UPDATE affiliate_partners SET
        referral_user_count = (SELECT count(*) FROM users WHERE affiliate_partners.id = users.affiliate_referral_id),
        cumulative_earnings_in_cents = (SELECT COALESCE(SUM(due_amount_in_cents), 0) FROM affiliate_quarterly_payments AS aqp WHERE  aqp.affiliate_partner_id = affiliate_partners.id AND closed = TRUE and paid = TRUE)
     }
    ActiveRecord::Base.connection.execute(sql)
  end

  def self.tally_traffic_totals(last_tallied_at, target_day)

    if last_tallied_at
      start_date = last_tallied_at.to_date
      end_date = target_day.to_date
    else
      start_date = target_day.to_date - 1
      end_date = target_day.to_date
    end

    if start_date == end_date
      return
    end

    sql = %{
      INSERT INTO affiliate_traffic_totals(SELECT day, 0, 0, ap.id FROM affiliate_partners AS ap CROSS JOIN (select (generate_series('#{start_date}', '#{end_date - 1}', '1 day'::interval))::date as day) AS lurp)
    }
    ActiveRecord::Base.connection.execute(sql)

    sql = %{
      UPDATE affiliate_traffic_totals traffic SET visits = COALESCE((SELECT COALESCE(count(affiliate_partner_id), 0) FROM affiliate_referral_visits v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_partner_id = traffic.affiliate_partner_id GROUP BY affiliate_partner_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}')
     }
    ActiveRecord::Base.connection.execute(sql)

    sql = %{
      UPDATE affiliate_traffic_totals traffic SET signups = COALESCE((SELECT COALESCE(count(v.id), 0) FROM users v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_referral_id = traffic.affiliate_partner_id GROUP BY affiliate_referral_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}')
     }
    ActiveRecord::Base.connection.execute(sql)
  end

  def self.boundary_dates(year, quarter)
    if quarter == 0
      [Date.new(year, 1, 1), Date.new(year, 3, 31)]
    elsif quarter == 1
      [Date.new(year, 4, 1), Date.new(year, 6, 30)]
    elsif quarter == 2
      [Date.new(year, 7, 1), Date.new(year, 9, 30)]
    elsif quarter == 3
      [Date.new(year, 10, 1), Date.new(year, 12, 31)]
    else
      raise "invalid quarter #{quarter}"
    end
  end

  # 1-based month
  def self.boundary_dates_for_month(year, month)
      [Date.new(year, month, 1), Date.civil(year, month, -1)]
  end

  # Finds all affiliates that need to be paid
  def self.unpaid

    joins(:quarters)
        .where('affiliate_quarterly_payments.paid = false').where('affiliate_quarterly_payments.closed = true')
        .group('affiliate_partners.id')
        .having('sum(due_amount_in_cents) >= ?', PAY_THRESHOLD)
        .order('sum(due_amount_in_cents) DESC')

  end

  # does this one affiliate need to be paid?
  def unpaid
    due_amount_in_cents > PAY_THRESHOLD
  end

  # admin function: mark the affiliate paid
  def mark_paid
    if unpaid
      transaction do
        now = Time.now
        quarters.where(paid:false, closed:true).update_all(paid:true, paid_at: now)
        self.last_paid_at = now
        self.save!
      end
    end
  end

  # how much is this affiliate due?
  def due_amount_in_cents
    total_in_cents = 0
    quarters.where(paid:false, closed:true).each do |quarter|
      total_in_cents = total_in_cents + quarter.due_amount_in_cents
    end
    total_in_cents
  end

  def affiliate_query_params
    AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s
  end

  def to_s
    display_name
  end
end
