require 'recurly'
module JamRuby
  class RecurlyClient
    def initialize()
      @log = Logging.logger[self]
    end

    def create_account(current_user, billing_info)
      options = account_hash(current_user, billing_info)
      account = nil
      begin
        #puts "Recurly.api_key: #{Recurly.api_key}"
        account = Recurly::Account.create(options)
        if account.errors.any?
          puts "Errors encountered while creating account: #{account.errors}"
          raise RecurlyClientError.new(account.errors) if account.errors.any?
        end
      rescue Recurly::Error, NoMethodError => x
        raise RecurlyClientError, x.to_s
      else
        if account
          current_user.update_attribute(:recurly_code, account.account_code)
        end
      end
      account
    end

    def has_account?(current_user)
      account = get_account(current_user)
      !!account
    end

    def delete_account(current_user)
      account = get_account(current_user)
      if account
        begin
          account.destroy
        rescue Recurly::Error, NoMethodError => x
          raise RecurlyClientError, x.to_s
        end
      else
        raise RecurlyClientError, "Could not find account to delete."
      end
      account
    end


    def update_desired_subscription(current_user, plan_code)
      subscription = nil
      account = nil
      current_user.desired_plan_code = plan_code
      current_user.desired_plan_code_set_at = DateTime.now
      current_user.save(validate: false)

      puts "updating desired subscription for #{current_user.email} to #{plan_code}"

      account = get_account(current_user)

      if account
        if plan_code.nil? || plan_code == ''
          begin
          # user wants a free subscription. If they have a subscription, let's cancel it.
            subscription, account = find_subscription(current_user, account)
            if subscription
              puts "Canceling user's #{current_user.email} subscription"
              subscription.cancel
              # upon cancelation, take the user's current monthly payment plan
              subscription = Recurly::Subscription.find(subscription.uuid)
              current_user.subscription_plan_code = get_highest_plan(subscription)
              current_user.subscription_plan_code_set_at = DateTime.now
              current_user.save(validate: false)
              # do not delete the recurly_subscription_id ; we'll use that to try and reactivate later if they user re-activates their account
            else
              # if no subscription and past trial, you goin down -- because there must have never been payment??
              if current_user.subscription_trial_ended?
                current_user.subscription_plan_code = nil
                current_user.subscription_plan_code_set_at = DateTime.now
                current_user.save(validate: false)
              end
            end

            # do not set the subscription _plan_code either; because the user has paid through the month; they still
            # get their old plan
            #current_user.subscription_plan_code = nil
            #current_user.save(validate: false)
          rescue => e
            puts "Could not cancel subscription for user #{current_user.email}. #{e}"
            return false, subscription, account
          end
        else
            # user wants to pay. let's get it goin
            return handle_create_subscription(current_user, plan_code, account)
        end
      end

      return true, subscription, account

    end


    def get_account(current_user)
      begin
        account = current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil
      rescue Recurly::Error => x
        puts "Swallow find acct for user #{current_user.email} error initial #{x}"
      end

      # check again, assuming account_code is the user ID (can happen in error scenarios where we create the account
      # on recurly, but couldn't save the account_code to the user.recurly_code  field)

      puts "get_account for #{current_user.email} found #{account}"
      if !account
        begin
          account = Recurly::Account.find(current_user.id)
        rescue Recurly::Error => x
          puts "Swallow find acct for user #{current_user.email}  error #{x}"
        end

        # repair user local account info
        if !account.nil?
          current_user.update_attribute(:recurly_code, account.account_code)
        end
      end

      account

    rescue Recurly::Error => x
      raise RecurlyClientError, x.to_s
    end


    def update_account(current_user, billing_info=nil)
      account = get_account(current_user)
      if(account.present?)
        options = account_hash(current_user, billing_info)
        begin
          account.update_attributes(options)
        rescue Recurly::Error, NoMethodError => x
          raise RecurlyClientError, x.to_s
        end
      end
      account
    end

    def list_invoices(account)
      invoices = []
      count = 0
      account.invoices.find_each do |invoice|
        count = count + 1
        invoices << invoice
        if count == 50
          break
        end
      end
      invoices
    end


    def payment_history(current_user, params ={})

      limit = params[:limit]
      limit ||= 20
      limit = limit.to_i

      cursor = params[:cursor]

      payments = []
      account = get_account(current_user)
      if(account.present?)
        begin

          account.transactions.paginate(per_page:limit, cursor:cursor).each do |transaction|
            # XXX this isn't correct because we create 0 dollar transactions too (for free stuff)
            #if transaction.amount_in_cents > 0 # Account creation adds a transaction record
              payments << {
                :created_at => transaction.created_at,
                :amount_in_cents => transaction.amount_in_cents,
                :tax_in_cents=> transaction.tax_in_cents,
                :status => transaction.status,
                :action => transaction.action,
                :payment_method => transaction.payment_method,
                :reference => transaction.reference,
                :currency => transaction.currency
              }
            #end
          end
        rescue Recurly::Error, NoMethodError => x
          puts "Recurly error #{current_user.email} #{x}"
          raise RecurlyClientError, x.to_s
        end
      end
      payments
    end


    def invoice_history(current_user, params ={})

      limit = params[:limit]
      limit ||= 20
      limit = limit.to_i

      cursor = params[:cursor]

      payments = []
      account = get_account(current_user)
      if(account.present?)
        begin

          account.invoices.paginate(per_page:limit, cursor:cursor).each do |invoice|
            # XXX this isn't correct because we create 0 dollar transactions too (for free stuff)
            #if transaction.amount_in_cents > 0 # Account creation adds a transaction record
            payments << {
                :created_at => invoice.created_at,
                :subtotal_in_cents => invoice.subtotal_in_cents,
                :tax_in_cents=> invoice.tax_in_cents,
                :total_in_cents => invoice.total_in_cents,
                :state => invoice.state,
                :description => invoice.line_items.map(&:description).join(", "),
                :currency => invoice.currency
            }
            #end
          end
        rescue Recurly::Error, NoMethodError => x
          puts "Recurly error #{current_user.email} #{x}"
          raise RecurlyClientError, x.to_s
        end
      end
      return payments, account
    end

    def update_billing_info(current_user, billing_info=nil, account = nil)
      account = get_account(current_user) if account.nil?
      if account.present?
        begin
          account.billing_info = billing_info
          account.billing_info.save
        rescue Recurly::Error, NoMethodError => x
          raise RecurlyClientError, x.to_s
        end

        raise RecurlyClientError.new(account.errors) if account.errors.any?
      else
        raise RecurlyClientError, "Could not find account to update billing info."
      end
      account
    end

    # token was created in the web ui.  we can tell recurly to update the billing info on the account with just the token
    def update_billing_info_from_token(current_user, account, recurly_token)
      account.billing_info = {
          token_id: recurly_token
      }
      account.billing_info.save!
    end

    def refund_user_subscription(current_user, jam_track)
      jam_track_right=JamRuby::JamTrackRight.where("user_id=? AND jam_track_id=?", current_user.id, jam_track.id).first
      if jam_track_right
        refund_subscription(jam_track_right)
      else
         raise RecurlyClientError, "The user #{current_user} does not have a subscription to #{jam_track}"
      end
    end

    def refund_subscription(jam_track_right)
      account = get_account(jam_track_right.user)
      if (account.present?)
        terminated = false
        begin
          jam_track = jam_track_right.jam_track
          account.subscriptions.find_each do |subscription|
            #puts "subscription.plan.plan_code: #{subscription.plan.plan_code} / #{jam_track.plan_code} / #{subscription.plan.plan_code == jam_track.plan_code}"
            if(subscription.plan.plan_code == jam_track.plan_code)
              subscription.terminate(:full)
              raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
              terminated = true
            end
          end

          if terminated
            jam_track_right.destroy()
          else
            raise RecurlyClientError, "Subscription '#{jam_track.plan_code}' not found for this user; could not issue refund."
          end

        rescue Recurly::Error, NoMethodError => x
          raise RecurlyClientError, x.to_s
        end

      else
        raise RecurlyClientError, "Could not find account to refund order."
      end
      account
    end

    def find_jam_track_plan(jam_track)
      plan = nil
      begin
        plan = Recurly::Plan.find(jam_track.plan_code)
      rescue Recurly::Resource::NotFound
      end
      plan
    end

    def create_jam_track_plan(jam_track)
      plan = Recurly::Plan.create(accounting_code: "",
                           bypass_hosted_confirmation: false,
                           cancel_url: nil,
                           description: jam_track.description,
                           display_donation_amounts: false,
                           display_phone_number: false,
                           display_quantity: false,
                           name: "JamTrack: #{jam_track.name}",
                           payment_page_css: nil,
                           payment_page_tos_link: nil,
                           plan_code: jam_track.plan_code,
                           plan_interval_length: 1,
                           plan_interval_unit: "months",
                           setup_fee_in_cents: Recurly::Money.new(:USD => 0), # <Recurly::Money USD: 0_00>
                           success_url: "",
                           tax_exempt: false,
                           total_billing_cycles: 1,
                           trial_interval_length: 0,
                           trial_interval_unit: "days",
                           unit_amount_in_cents: Recurly::Money.new(:USD => 1_99),
                           unit_name: "unit"
                  )
      raise RecurlyClientError.new(plan.errors) if plan.errors.any?
    end

    def get_pending_plan_code(subscription)
      if subscription && subscription.pending_subscription
        return subscription.pending_subscription.plan.plan_code
      else
        return nil
      end
    end

    def get_highest_plan(subscription)
      SubscriptionDefinitions.higher_plan(subscription.plan.plan_code, get_pending_plan_code(subscription))
    end

    def handle_create_subscription(current_user, plan_code, account)
      begin
        subscription = create_subscription(current_user, plan_code, account, current_user.subscription_trial_ended? ? nil : current_user.subscription_trial_ends_at)
        current_user.recurly_subscription_id = subscription.uuid
        if current_user.subscription_trial_ended?
          current_user.subscription_plan_code = get_highest_plan(subscription)
          current_user.subscription_plan_code_set_at = DateTime.now
        else
          # we could force a platinum plan since the user has put forward payment already, even in trial
          puts "user #{current_user.email} is in trial"
          if plan_code == SubscriptionDefinitions::JAM_PLATINUM || plan_code == SubscriptionDefinitions::JAM_PLATINUM_YEARLY
            puts "user #{current_user.email} is in trial and buying platinum ; upgrade them already"
            current_user.subscription_plan_code = plan_code
            current_user.subscription_plan_code_set_at = DateTime.now
          else
            current_user.subscription_plan_code = SubscriptionDefinitions::JAM_GOLD
            current_user.subscription_plan_code_set_at = DateTime.now
          end

        end

        current_user.save(validate: false)
      rescue => e
        puts "Could not create subscription for user #{current_user.email}. #{e}"
        return false, subscription, account
      end

      return true, subscription, account
    end

    # https://dev.recurly.com/docs/create-subscription
    def create_subscription(user, plan_code, account, starts_at = nil)

      old_subscription_id = user.recurly_subscription_id

      if old_subscription_id
        # first, let's try to reactivate it
        old_subscription = Recurly::Subscription.find(old_subscription_id)
        begin
          old_subscription.reactivate
          puts "reactivated plan! Let's check if it needs changing"
          #if plan_code != old_subscription.plan.plan_code
            result = old_subscription.update_attributes(
                :plan_code => plan_code,
                :timeframe => starts_at.nil? ? 'bill_date' : 'now'
            )
          #  end
          # fetch it again. because it's got staleness after update_attributes operation
          return  Recurly::Subscription.find(old_subscription_id)
        rescue => e
          puts "Unable to reactivate/update old plan #{e}"
          user.update_attribute(:recurly_subscription_id, nil)
        end
      end

      if account.billing_info
        puts "Creating subscription for #{user.email} with plan_code #{plan_code}"
        subscription = Recurly::Subscription.create(
            :plan_code => plan_code,
            :currency  => 'USD',
            :customer_notes  => 'Thank you for your business!',
            :account   => {
                :account_code => account.account_code
            },
            :starts_at => starts_at,
            :auto_renew => true
        )
        subscription
      else
        puts "User has no billing info; not trying to create a subscription #{user.email}"
      end
      subscription
    end

    def find_subscription(user, fed_account = nil)
      subscription = nil
      account = nil

      if fed_account.nil?
        account = get_account(user)
      else
        account = fed_account
      end

      # first try to find the current subscription. If it's gone, delete our state. If expired, delete our state.
      if user.recurly_subscription_id
        begin
          subscription = Recurly::Subscription.find(user.recurly_subscription_id)
        rescue Recurly::Resource::NotFound
          puts "subscription is gone. delete it!"
          user.update_attribute(:recurly_subscription_id, nil)
          user.recurly_subscription_id = nil
          subscription = nil
        end

        puts "Subscription state: #{subscription.state}"
        if subscription.state == 'expired'
          puts "subscription is expired. stop tracking it!"
          user.update_attribute(:recurly_subscription_id, nil)
          user.recurly_subscription_id = nil
          subscription = nil
        end
      end

      if user.recurly_subscription_id.nil?
        if account
          active_subscription = nil
          account.subscriptions.find_each do |subscription|
            puts "Subscription: #{subscription.inspect} #{subscription.state}"
            if subscription.state == "active" || subscription.state == "future"
              active_subscription = subscription
              break
            end
          end
          subscription = active_subscription
        else
          puts "can't find subscription for account #{account}"
        end
      end

      if subscription && user.recurly_subscription_id.nil?
        puts "Repairing subscription ID on account"
        user.update_attribute(:recurly_subscription_id, subscription.uuid)
        user.recurly_subscription_id = subscription.uuid
      end

      return [subscription, account]
    end

    def change_subscription_plan(current_user, plan_code)
      subscription, account = find_subscription(current_user)

      if subscription.nil?
        puts "no subscription found for user #{current_user.email}"
        return false
      end

      puts "subscription.plan #{subscription.plan}"
      if subscription.plan.plan_code == plan_code
        puts "plan code was the same as requested: #{plan_code}"
        return false
      end

      result = subscription.update_attributes(
          :plan_code => plan_code,
          :timeframe => 'bill_date'
      )
      puts "change subscription plan #{result}"

      return result
    end


    def sync_subscription(user)

      begin
        # edge case: admin controlled
        if user.admin_override_plan_code
          # check if it's expired first...
          if Time.now > user.admin_override_ends_at.to_time
            puts "admin control expired. clear override and set Free plan"
            user.admin_override_plan_code = nil
            # logic below will catch this
            #user.subscription_plan_code = nil
            user.admin_override_ends_at = nil
            user.subscription_sync_code = 'undo_admin_control'
            user.subscription_sync_msg = "admin control expired. clear override and set Free plan"
            user.subscription_last_checked_at = Time.now
            user.save(validate: false)
            # don't return; let this fall through to next states
          else
            puts "admin controlled plan #{user.email}"
            user.subscription_plan_code = user.admin_override_plan_code
            user.subscription_plan_code_set_at = Time.now
            user.subscription_last_checked_at = Time.now
            user.subscription_sync_code = 'admin_control'
            user.subscription_sync_msg = "admin override - plan_code set to #{user.admin_override_plan_code}"
            user.save(validate: false)
            return
          end

        end

        # edge case: user is in a licensed school
        if user.has_active_license?
          puts "user has school license #{user.email}"
          user.subscription_plan_code = SubscriptionDefinitions::JAM_PLATINUM
          user.subscription_plan_code_set_at = DateTime.now
          user.subscription_last_checked_at = DateTime.now
          user.subscription_sync_code = 'school_license'
          user.subscription_sync_msg = "has school license - plan_code set to #{SubscriptionDefinitions::JAM_PLATINUM}"
          user.save(validate: false)
          return
        end

        # if user is in trial still, not much book-keeping
        if !user.subscription_trial_ended?
          puts "user has a trial still #{user.email}"
          # there is actually nothing to do, because we don't start billing for any plan until trial is over.
          user.subscription_last_checked_at = DateTime.now
          user.subscription_sync_code = 'in_trial'
          user.subscription_sync_msg = "trial still active - plan_code not altered"
          user.save(validate: false)
          return
        end

        # if there is no recurly action here, then they must be coming off of a trial and we have to mark them down
        if user.recurly_code.nil? && !user.subscription_plan_code.nil?
          puts "new user #{user.email} has no payment info and is ending their trial"
          # TODO: send email
          user.subscription_plan_code = nil
          user.subscription_plan_code_set_at = DateTime.now
          user.subscription_last_checked_at = DateTime.now
          user.subscription_sync_code = 'trial_ended'
          user.subscription_sync_msg = "trial ended and no subscription set - plan_code set to Free"
          user.save(validate: false)
          return
        end

        account = get_account(user)

        if account.nil?
          puts "Account is nil? #{user.email}. Strange. Could happen in some weird admin messing around scenarios"
          user.subscription_last_checked_at = DateTime.now
          user.save(validate: false)
          user.subscription_sync_code = 'no_recurly_account'
          user.subscription_sync_msg = "user has no recurly account - plan_code not altered"
          user.save(validate: false)
          return
        end

        user.is_past_due = account.has_past_due_invoice

        subscription, account = find_subscription(user, account)
        if subscription
          user.recurly_subscription_state = subscription.state
        else
          user.recurly_subscription_state = nil
        end


        if subscription.nil? || subscription.state == 'expired'
          puts "user has expired or no plan"
          user.subscription_plan_code = nil
          user.subscription_plan_code_set_at = DateTime.now
          user.subscription_sync_code = 'no_subscription_or_expired'
          user.subscription_sync_msg = "user has no or expired subscription - plan_code set to Free"
        else
          if user.is_past_due
            if !user.subscription_plan_code.nil?
              puts "user #{user.email} has a past due plan. We gotta bring them down"
              user.subscription_plan_code = nil
              user.subscription_plan_code_set_at = DateTime.now
              user.subscription_sync_code = 'is_past_due_changed'
              user.subscription_sync_msg = "payment has gone past due - plan_code set to Free"
            else
              puts "user is past due and #{user.email} had no changes"
              user.subscription_sync_code = 'is_past_due_unchanged'
              user.subscription_sync_msg = "payment has gone past due, plan_code not altered because already set to free"
            end
          else
            if user.subscription_plan_code != user.desired_plan_code
              puts "they are back!  get them back into their desired plan #{user.email}"
              if !SubscriptionDefinitions.is_downgrade(user.desired_plan_code, user.subscription_plan_code)
                user.subscription_plan_code = user.desired_plan_code
                user.subscription_plan_code_set_at = DateTime.now
                user.subscription_sync_code = 'good_standing_repaired'
                user.subscription_sync_msg = "user is in good standing but desired != effective; plan_code set to #{user.desired_plan_code}"
              else
                #user.subscription_plan_code = user.desired_plan_code
                #user.subscription_plan_code_set_at = DateTime.now
                user.subscription_sync_code = 'good_standing_ignored'
                user.subscription_sync_msg = "user is in good standing but the desired plan is less than subscription plan; plan_code not touched"
              end
            else
              puts "good standing user #{user.email} had no changes"
              user.subscription_sync_code = 'good_standing_unchanged'
              user.subscription_sync_msg = "user is in good standing but already set correctly; plan_code not altered"
            end

          end
        end

        user.subscription_last_checked_at = DateTime.now
        user.save(validate: false)
      rescue => e
        puts "Unexpected error in sync_subscription for user #{user.email}"
        puts e.message
        user.subscription_last_checked_at = DateTime.now
        user.subscription_sync_code = 'failed_sync'
        user.subscription_sync_msg = e.message
        user.save(validate: false)
      end

    end

    def find_or_create_account(current_user, billing_info, recurly_token = nil)
      account = get_account(current_user)

      if !account
        account = create_account(current_user, billing_info)
      elsif !billing_info.nil?
        update_billing_info(current_user, billing_info, account)
      end

      if !recurly_token.nil?
        update_billing_info_from_token(current_user, account, recurly_token)
      end
      account
    end

  private
    def account_hash(current_user, billing_info)
      options = {
        account_code: current_user.id,
        email:        current_user.email,
        first_name:   current_user.first_name,
        last_name:    current_user.last_name,
        address: {
          city: current_user.city,
          state: current_user.state,
          country: current_user.country
        }
      }

      options[:billing_info] = billing_info if billing_info

      options
    end
  end # class

  class RecurlyClientError < Exception
    attr_accessor :errors
    def initialize(data)
      if data.respond_to?('has_key?')
        self.errors = data
      else
        self.errors = {:message=>data.to_s}
      end
    end # initialize

    def to_s
      s=super
      s << ", errors: #{errors.inspect}" if self.errors.any?
      s
    end

  end # RecurlyClientError
end # module
  