# frozen_string_literal: true

require "pg/em"
require "bigdecimal"
require "blather/client"
require "braintree"
require "dhall"
require "em-hiredis"
require "em_promise"

require_relative "lib/btc_sell_prices"
require_relative "lib/buy_account_credit_form"
require_relative "lib/customer"
require_relative "lib/electrum"
require_relative "lib/em"
require_relative "lib/payment_methods"
require_relative "lib/registration"
require_relative "lib/transaction"
require_relative "lib/web_register_manager"

CONFIG =
	Dhall::Coder
	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
	.load(ARGV[0], transform_keys: ->(k) { k&.to_sym })

ELECTRUM = Electrum.new(**CONFIG[:electrum])

# Braintree is not async, so wrap in EM.defer for now
class AsyncBraintree
	def initialize(environment:, merchant_id:, public_key:, private_key:, **)
		@gateway = Braintree::Gateway.new(
			environment: environment,
			merchant_id: merchant_id,
			public_key: public_key,
			private_key: private_key
		)
	end

	def respond_to_missing?(m, *)
		@gateway.respond_to?(m)
	end

	def method_missing(m, *args)
		return super unless respond_to_missing?(m, *args)

		EM.promise_defer(klass: PromiseChain) do
			@gateway.public_send(m, *args)
		end
	end

	class PromiseChain < EMPromise
		def respond_to_missing?(*)
			false # We don't actually know what we respond to...
		end

		def method_missing(m, *args)
			return super if respond_to_missing?(m, *args)
			self.then { |o| o.public_send(m, *args) }
		end
	end
end

BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])

Blather::DSL.append_features(self.class)

def panic(e)
	warn "Error raised during event loop: #{e.message}"
	warn e.backtrace if e.respond_to?(:backtrace)
	exit 1
end

EM.error_handler(&method(:panic))

when_ready do
	BLATHER = self
	REDIS = EM::Hiredis.connect
	BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id])
	DB = PG::EM::Client.new(dbname: "jmp")
	DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
	DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)

	EM.add_periodic_timer(3600) do
		ping = Blather::Stanza::Iq::Ping.new(:get, CONFIG[:server][:host])
		ping.from = CONFIG[:component][:jid]
		self << ping
	end
end

# workqueue_count MUST be 0 or else Blather uses threads!
setup(
	CONFIG[:component][:jid],
	CONFIG[:component][:secret],
	CONFIG[:server][:host],
	CONFIG[:server][:port],
	nil,
	nil,
	workqueue_count: 0
)

message :error? do |m|
	puts "MESSAGE ERROR: #{m.inspect}"
end

class SessionManager
	def initialize(blather, id_msg, timeout: 5)
		@blather = blather
		@sessions = {}
		@id_msg = id_msg
		@timeout = timeout
	end

	def promise_for(stanza)
		id = "#{stanza.to.stripped}/#{stanza.public_send(@id_msg)}"
		@sessions.fetch(id) do
			@sessions[id] = EMPromise.new
			EM.add_timer(@timeout) do
				@sessions.delete(id)&.reject(:timeout)
			end
			@sessions[id]
		end
	end

	def write(stanza)
		promise = promise_for(stanza)
		@blather << stanza
		promise
	end

	def fulfill(stanza)
		id = "#{stanza.from.stripped}/#{stanza.public_send(@id_msg)}"
		if stanza.error?
			@sessions.delete(id)&.reject(stanza)
		else
			@sessions.delete(id)&.fulfill(stanza)
		end
	end
end

IQ_MANAGER = SessionManager.new(self, :id)
COMMAND_MANAGER = SessionManager.new(self, :sessionid, timeout: 60 * 60)
web_register_manager = WebRegisterManager.new

disco_items node: "http://jabber.org/protocol/commands" do |iq|
	reply = iq.reply
	reply.items = [
		# TODO: don't show this item if no braintree methods available
		# TODO: don't show this item if no plan for this customer
		Blather::Stanza::DiscoItems::Item.new(
			iq.to,
			"buy-credit",
			"Buy account credit"
		),
		Blather::Stanza::DiscoItems::Item.new(
			iq.to,
			"jabber:iq:register",
			"Register"
		)
	]
	self << reply
end

command :execute?, node: "jabber:iq:register", sessionid: nil do |iq|
	Customer.for_jid(iq.from.stripped).catch {
		nil
	}.then { |customer|
		Registration.for(
			iq,
			customer,
			web_register_manager
		).then(&:write)
	}.catch(&method(:panic))
end

def reply_with_note(iq, text, type: :info)
	reply = iq.reply
	reply.status = :completed
	reply.note_type = type
	reply.note_text = text

	self << reply
end

command :execute?, node: "buy-credit", sessionid: nil do |iq|
	reply = iq.reply
	reply.allowed_actions = [:complete]

	Customer.for_jid(iq.from.stripped).then { |customer|
		BuyAccountCreditForm.new(customer).add_to_form(reply.form).then { customer }
	}.then { |customer|
		EMPromise.all([
			customer.payment_methods,
			customer.merchant_account,
			COMMAND_MANAGER.write(reply)
		])
	}.then { |(payment_methods, merchant_account, iq2)|
		iq = iq2 # This allows the catch to use it also
		payment_method = payment_methods.fetch(
			iq.form.field("payment_method")&.value.to_i
		)
		amount = iq.form.field("amount").value.to_s
		Transaction.sale(merchant_account, payment_method, amount)
	}.then { |transaction|
		transaction.insert.then { transaction.amount }
	}.then { |amount|
		reply_with_note(iq, "$#{'%.2f' % amount} added to your account balance.")
	}.catch { |e|
		text = "Failed to buy credit, system said: #{e.message}"
		reply_with_note(iq, text, type: :error)
	}.catch(&method(:panic))
end

command sessionid: /./ do |iq|
	COMMAND_MANAGER.fulfill(iq)
end

iq :result? do |iq|
	IQ_MANAGER.fulfill(iq)
end

iq :error? do |iq|
	IQ_MANAGER.fulfill(iq)
end
