# frozen_string_literal: true

require "bigdecimal/util"
require "delegate"

require_relative "transaction"
require_relative "trust_level_repo"

class TransactionDeclinedError < StandardError; end

# Error raised when a transaction amount exceeds the maximum allowed limit.
class AmountTooHighError < TransactionDeclinedError
	# @return [Numeric] The transaction amount that was too high.
	attr_reader :amount
	# @return [Numeric] The maximum amount allowed for the transaction.
	attr_reader :max_amount

	# Initializes a new AmountTooHighError.
	# @param amount [Numeric] The transaction amount.
	# @param max_amount [Numeric] The maximum allowed amount.
	def initialize(amount, max_amount)
		@amount = amount
		@max_amount = max_amount
		super("Amount $#{amount} exceeds maximum allowed amount of $#{max_amount}")
	end
end

# Error raised when a transaction amount is below the minimum required limit.
class AmountTooLowError < TransactionDeclinedError
	# @return [Numeric] The transaction amount that was too low.
	attr_reader :amount
	# @return [Numeric] The minimum amount required for the transaction.
	attr_reader :min_amount

	# Initializes a new AmountTooLowError.
	# @param amount [Numeric] The transaction amount.
	# @param min_amount [Numeric] The minimum required amount.
	def initialize(amount, min_amount)
		@amount = amount
		@min_amount = min_amount
		super("Amount $#{amount} is below minimum amount of $#{min_amount}")
	end
end

# Error raised when a transaction is declined, potentially due to exceeding
# decline limits.
class DeclinedError < TransactionDeclinedError
	# @return [Integer, nil] The number of declines the customer has.
	attr_reader :declines
	# @return [Integer, nil] The maximum number of declines allowed.
	attr_reader :max_declines

	# Initializes a new DeclinedError.
	# @param declines [Integer, nil] (nil) The current number of declines.
	# @param max_declines [Integer, nil] (nil) The maximum allowed declines.
	def initialize(declines=nil, max_declines=nil)
		@declines = declines
		@max_declines = max_declines
		super("Transaction declined")
	end
end

class CreditCardSale
	def self.create(*args, transaction_class: Transaction, **kwargs)
		new(*args, **kwargs).sale.then do |response|
			tx = BraintreeTransaction.build(
				response,
				transaction_class: transaction_class
			)
			tx.insert.then { tx }
		end
	end

	class BraintreeTransaction < SimpleDelegator
		def self.build(braintree_transaction, transaction_class: Transaction)
			new(braintree_transaction).to_transaction(transaction_class)
		end

		def to_transaction(transaction_class)
			transaction_class.new(
				customer_id: customer_details.id,
				transaction_id: id,
				created_at: created_at,
				settled_after: created_at + (90 * 24 * 60 * 60),
				amount: amount,
				note: "Credit card payment"
			)
		end
	end

	def initialize(
		customer, amount:, payment_method: nil,
		trust_repo: TrustLevelRepo.new
	)
		@customer = customer
		@amount = amount
		@payment_method = payment_method
		@trust_repo = trust_repo
	end

	def sale
		EMPromise.all([validate!, resolve_payment_method]).then { |_, selected|
			BRAINTREE.transaction.sale(
				amount: @amount,
				merchant_account_id: @customer.merchant_account,
				options: { submit_for_settlement: true },
				payment_method_token: selected.token
			)
		}.then { |response| decline_guard(response) }
	end

protected

	# Validates the transaction against customer locks, trust level, and decline
	# history.
	# @raise [TransactionDeclinedError] if the customer has made too many payments
	# recently.
	# @raise [AmountTooHighError] if the amount exceeds the trust level's maximum
	# top-up amount.
	# @raise [AmountTooLowError] if the amount is below any applicable minimum.
	# @raise [DeclinedError] if the transaction is declined due to too many
	# previous declines or other trust level restrictions.
	# @return [EMPromise<void>] A promise that resolves if validation passes, or
	# rejects with an error.
	def validate!
		EMPromise.all([
			REDIS.exists("jmp_customer_credit_card_lock-#{@customer.customer_id}"),
			@trust_repo.find(@customer), @customer.declines
		]).then do |(lock, tl, declines)|
			raise TransactionDeclinedError, "Too many payments recently" if lock == 1

			tl.validate_credit_card_transaction!(@amount.to_d, declines)
		end
	end

	def resolve_payment_method
		EMPromise.all([
			@payment_method ||
				@customer.payment_methods.then(&:default_payment_method)
		]).then do |(selected_method)|
			raise "No valid payment method on file" unless selected_method

			selected_method
		end
	end

	def churnbuster_success(response)
		Churnbuster.new.successful_payment(
			@customer,
			@amount,
			response.transaction.id
		)
	end

	def decline_guard(response)
		if response.success?
			churnbuster_success(response)
			REDIS.setex(
				"jmp_customer_credit_card_lock-#{@customer.customer_id}",
				60 * 60 * 24, "1"
			)
			return response.transaction
		end

		@customer.mark_decline
		raise BraintreeFailure, response
	end
end

class BraintreeFailure < StandardError
	attr_reader :response

	def initialize(response)
		super(response.message)
		@response = response
	end
end
