# frozen_string_literal: true

require "pg/em/connection_pool"
require "bandwidth"
require "bigdecimal"
require "blather/client/dsl"
require "date"
require "dhall"
require "em-hiredis"
require "em_promise"
require "ougai"
require "ruby-bandwidth-iris"
require "sentry-ruby"
require "statsd-instrument"

require_relative "lib/background_log"
require_relative "lib/utils"

$stdout.sync = true
LOG = Ougai::Logger.new(BackgroundLog.new($stdout))
LOG.level = ENV.fetch("LOG_LEVEL", "info")
LOG.formatter = Ougai::Formatters::Readable.new(
	nil,
	nil,
	plain: !$stdout.isatty
)
Blather.logger = LOG
EM::Hiredis.logger = LOG
StatsD.logger = LOG
LOG.info "Starting"

def log
	Thread.current[:log] || LOG
end

Sentry.init do |config|
	config.logger = LOG
	config.breadcrumbs_logger = [:sentry_logger]
end

CONFIG = Dhall::Coder
	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
	.load(
		"(#{ARGV[0]}) : #{__dir__}/config-schema.dhall",
		transform_keys: ->(k) { k&.to_sym },
		timeout: 30
	)
WEB_LISTEN =
	if CONFIG[:web].is_a?(Hash)
		[CONFIG[:web][:interface], CONFIG[:web][:port]]
	else
		[CONFIG[:web]]
	end

singleton_class.class_eval do
	include Blather::DSL
	Blather::DSL.append_features(self)
end

require_relative "lib/session_manager"

IQ_MANAGER = SessionManager.new(self, :id)
COMMAND_MANAGER = SessionManager.new(
	self,
	:sessionid,
	timeout: 60 * 60,
	error_if: ->(s) { s.cancel? }
)

require_relative "lib/polyfill"
require_relative "lib/alt_top_up_form"
require_relative "lib/admin_command"
require_relative "lib/backend_sgx"
require_relative "lib/bwmsgsv2_repo"
require_relative "lib/bandwidth_iris_patch"
require_relative "lib/bandwidth_tn_order"
require_relative "lib/bandwidth_tn_repo"
require_relative "lib/btc_sell_prices"
require_relative "lib/buy_account_credit_form"
require_relative "lib/configure_calls_form"
require_relative "lib/command"
require_relative "lib/command_list"
require_relative "lib/customer"
require_relative "lib/customer_info"
require_relative "lib/customer_info_form"
require_relative "lib/customer_repo"
require_relative "lib/dummy_command"
require_relative "lib/db_notification"
require_relative "lib/electrum"
require_relative "lib/empty_repo"
require_relative "lib/expiring_lock"
require_relative "lib/em"
require_relative "lib/form_to_h"
require_relative "lib/low_balance"
require_relative "lib/port_in_order"
require_relative "lib/patches_for_sentry"
require_relative "lib/payment_methods"
require_relative "lib/paypal_done"
require_relative "lib/postgres"
require_relative "lib/reachability_form"
require_relative "lib/reachability_repo"
require_relative "lib/registration"
require_relative "lib/transaction"
require_relative "lib/tel_selections"
require_relative "lib/sim_repo"
require_relative "lib/sim_order"
require_relative "lib/edit_sim_nicknames"
require_relative "lib/snikket"
require_relative "lib/welcome_message"
require_relative "web"
require_relative "lib/statsd"

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

LOG.info "Loading scripts from #{__dir__}/redis_lua"
EM::Hiredis::Client.load_scripts_from("#{__dir__}/redis_lua")

Faraday.default_adapter = :em_synchrony
BandwidthIris::Client.global_options = {
	account_id: CONFIG[:creds][:account],
	username: CONFIG[:creds][:username],
	password: CONFIG[:creds][:password]
}
BANDWIDTH_VOICE = Bandwidth::Client.new(
	voice_basic_auth_user_name: CONFIG[:creds][:username],
	voice_basic_auth_password: CONFIG[:creds][:password]
).voice_client.client

class AuthError < StandardError; end

require_relative "lib/async_braintree"
BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])

def panic(e, hub=nil)
	log.fatal(
		"Error raised during event loop: #{e.class}",
		e
	)
	if e.is_a?(::Exception)
		(hub || Sentry).capture_exception(e, hint: { background: false })
	else
		(hub || Sentry).capture_message(e.to_s, hint: { background: false })
	end
	exit 1
end

EM.error_handler(&method(:panic))

require_relative "lib/blather_client"
@client = BlatherClient.new

setup(
	CONFIG[:component][:jid],
	CONFIG[:component][:secret],
	CONFIG[:server][:host],
	CONFIG[:server][:port],
	nil,
	nil,
	async: true
)

# Infer anything we might have been notified about while we were down
def catchup_notify_low_balance(db)
	db.query(<<~SQL).each do |c|
		SELECT customer_id
		FROM balances INNER JOIN customer_plans USING (customer_id)
		WHERE balance < 5 AND expires_at > LOCALTIMESTAMP
	SQL
		db.query("SELECT pg_notify('low_balance', $1)", c.values)
	end
end

def catchup_notify_possible_renewal(db)
	db.query(<<~SQL).each do |c|
		SELECT customer_id
		FROM customer_plans INNER JOIN balances USING (customer_id)
		WHERE
			expires_at < LOCALTIMESTAMP
			AND expires_at >= LOCALTIMESTAMP - INTERVAL '3 months'
			AND balance >= 5
	SQL
		db.query("SELECT pg_notify('possible_renewal', $1)", c.values)
	end
end

def setup_sentry_scope(name)
	Sentry.clone_hub_to_current_thread
	Sentry.with_scope do |scope|
		scope.clear_breadcrumbs
		scope.set_transaction_name(name)
		Thread.current[:log] = ::LOG.child(transaction: scope.transaction_name)
		yield scope
	end
end

def poll_for_notify(db, repo)
	db.wait_for_notify_defer.then { |notify|
		setup_sentry_scope("DB NOTIFY") do
			repo.find(notify[:extra]).then { |customer|
				DbNotification.for(notify, customer, repo)
			}.then(&:call).catch { |e|
				log.error("Error during poll_for_notify", e)
				Sentry.capture_exception(e)
			}.sync
		end
	}.then { EM.add_timer(0.5) { poll_for_notify(db, repo) } }
end

def load_plans_to_db!
	DB.transaction do
		DB.exec("TRUNCATE plans")
		CONFIG[:plans].each do |plan|
			DB.exec("INSERT INTO plans VALUES ($1)", [plan.to_json])
		end
	end
end

when_ready do
	log.info "Ready"
	BLATHER = self
	REDIS = EM::Hiredis.connect
	MEMCACHE = EM::P::Memcache.connect
	BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id])
	BCH_SELL_PRICES = BCHSellPrices.new(REDIS, CONFIG[:oxr_app_id])
	DB = Postgres.connect(dbname: "jmp", size: 5)
	TEL_SELECTIONS = TelSelections.new

	EMPromise.resolve(nil).then {
		conn = DB.acquire
		conn.query("LISTEN low_balance")
		conn.query("LISTEN possible_renewal")
		catchup_notify_low_balance(conn)
		catchup_notify_possible_renewal(conn)

		repo = CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
		poll_for_notify(conn, repo)
	}.catch(&method(:panic))

	load_plans_to_db!

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

	Web.run(LOG.child, *WEB_LISTEN)
end

message to: /\Aaccount@/, body: /./ do |m|
	StatsD.increment("deprecated_account_bot")

	self << m.reply.tap { |out|
		out.body = "This bot is deprecated. Please talk to xmpp:cheogram.com"
	}
end

FROM_BACKEND =
	"(?:#{([CONFIG[:sgx]] + CONFIG[:sgx_creds].keys)
		.map(&Regexp.method(:escape)).join('|')})"

before(
	:iq,
	type: [:error, :result],
	to: /\Acustomer_/,
	from: /(\A|@)#{FROM_BACKEND}(\/|\Z)/
) { |iq| halt if IQ_MANAGER.fulfill(iq) }

before nil, to: /\Acustomer_/, from: /(\A|@)#{FROM_BACKEND}(\/|\Z)/ do |s|
	StatsD.increment("stanza_customer")

	Sentry.get_current_scope.set_transaction_name("stanza_customer")
	CustomerRepo.new(set_user: Sentry.method(:set_user)).find(
		s.to.node.delete_prefix("customer_")
	).then do |customer|
		ReachabilityRepo::SMS.new
			.find(customer, s.from.node, stanza: s).then do |reach|
				reach.filter do
					customer.stanza_to(s)
				end
			end
	end

	halt
end

ADDRESSES_NS = "http://jabber.org/protocol/address"
message(
	to: /\A#{CONFIG[:component][:jid]}\Z/,
	from: /(\A|@)#{FROM_BACKEND}(\/|\Z)/
) do |m|
	StatsD.increment("inbound_group_text")
	Sentry.get_current_scope.set_transaction_name("inbound_group_text")
	log.info "Possible group text #{m.from}"

	address = m.find("ns:addresses", ns: ADDRESSES_NS).first
		&.find("ns:address", ns: ADDRESSES_NS)
		&.find { |el| el["jid"].to_s.start_with?("customer_") }
	pass unless address

	CustomerRepo
		.new(set_user: Sentry.method(:set_user))
		.find_by_jid(address["jid"]).then { |customer|
			m.from = m.from.with(domain: CONFIG[:component][:jid])
			m.to = m.to.with(domain: customer.jid.domain)
			address["jid"] = customer.jid.to_s
			BLATHER << m
		}.catch_only(CustomerRepo::NotFound) { |e|
			BLATHER << m.as_error("forbidden", :auth, e.message)
		}
end

# Ignore groupchat messages
# Especially if we have the component join MUC for notifications
message(type: :groupchat) { true }

def billable_message(m)
	b = m.body
	b && !b.empty? || m.find("ns:x", ns: OOB.registered_ns).first
end

class OverLimit < StandardError
	def initialize(customer, usage)
		super("Please contact support")
		@customer = customer
		@usage = usage
	end

	def notify_admin
		ExpiringLock.new("jmp_usage_notify-#{@customer.customer_id}").with do
			BLATHER.join(CONFIG[:notify_admin], "sgx-jmp")
			BLATHER.say(
				CONFIG[:notify_admin], "#{@customer.customer_id} has used " \
				"#{@usage} messages today", :groupchat
			)
		end
	end
end

class CustomerExpired < StandardError; end

CONFIG[:direct_targets].each do |(tel, jid)|
	customer_repo = CustomerRepo.new(
		sgx_repo: TrivialBackendSgxRepo.new(jid: jid),
		set_user: Sentry.method(:set_user)
	)

	message to: /\A#{Regexp.escape(tel)}@#{CONFIG[:component][:jid]}\/?/ do |m|
		customer_repo.find_by_jid(m.from.stripped).then { |customer|
			customer.stanza_from(m)
		}.catch_only(CustomerRepo::NotFound) {
			# This should not happen, but let's still get the message
			# to support at least if it does
			m.from = ProxiedJID.proxy(m.from, CONFIG[:component][:jid])
			m.to = jid
			BLATHER << m
		}
	end
end

CONFIG[:direct_sources].each do |(jid, tel)|
	customer_repo = CustomerRepo.new(
		sgx_repo: TrivialBackendSgxRepo.new(jid: jid),
		set_user: Sentry.method(:set_user)
	)
	message to: /\Acustomer_/, from: /\A#{Regexp.escape(jid)}\/?/ do |m|
		customer_repo.find(m.to.node.delete_prefix("customer_")).then { |customer|
			m.from = "#{tel}@sgx-jmp" # stanza_to will fix domain
			customer.stanza_to(m)
		}.catch_only(CustomerRepo::NotFound) { |e|
			BLATHER << m.as_error("item-not-found", :cancel, e.message)
		}
	end
end

def find_from_and_to_customer(from, to)
	(
		# TODO: group text?
		to.node ? CustomerRepo.new.find_by_tel(to.node) : EMPromise.resolve(nil)
	).catch_only(CustomerRepo::NotFound) { nil }.then { |target_customer|
		sgx_repo = target_customer ? Bwmsgsv2Repo.new : TrivialBackendSgxRepo.new
		EMPromise.all([
			CustomerRepo.new(set_user: Sentry.method(:set_user), sgx_repo: sgx_repo)
				.find_by_jid(from.stripped),
			target_customer
		])
	}
end

message do |m|
	StatsD.increment("message")

	today = Time.now.utc.to_date
	find_from_and_to_customer(m.from, m.to).then { |(customer, target_customer)|
		if target_customer && customer.registered?
			m.from = "#{customer.registered?.phone}@sgx-jmp"
			next target_customer.stanza_to(m)
		end

		next customer.stanza_from(m) unless billable_message(m)

		if customer.plan_name && !customer.active?
			raise CustomerExpired, "Your account is expired, please top up"
		end

		EMPromise.all([
			TrustLevelRepo.new.find(customer),
			customer.message_usage((today..today))
		]).then { |(tl, usage)|
			raise OverLimit.new(customer, usage) unless tl.send_message?(usage)
		}.then do
			EMPromise.all([customer.incr_message_usage, customer.stanza_from(m)])
		end
	}.catch_only(OverLimit) { |e|
		e.notify_admin
		BLATHER << m.as_error("policy-violation", :wait, e.message)
	}.catch_only(CustomerRepo::NotFound, CustomerExpired) { |e|
		BLATHER << m.as_error("forbidden", :auth, e.message)
	}
end

disco_info to: Blather::JID.new(CONFIG[:component][:jid]) do |iq|
	reply = iq.reply
	reply.identities = [{
		name: "JMP.chat",
		type: "sms",
		category: "gateway"
	}]
	reply.features = [
		"http://jabber.org/protocol/disco#info",
		"http://jabber.org/protocol/commands"
	]
	form = Blather::Stanza::X.find_or_create(reply.query)
	form.type = "result"
	form.fields = [
		{
			var: "FORM_TYPE",
			type: "hidden",
			value: "http://jabber.org/network/serverinfo"
		}
	] + CONFIG[:xep0157]
	self << reply
end

disco_info do |iq|
	reply = iq.reply
	reply.identities = [{
		name: "JMP.chat",
		type: "sms",
		category: "client"
	}]
	reply.features = [
		"urn:xmpp:receipts"
	]
	self << reply
end

disco_items(
	to: Blather::JID.new(CONFIG[:component][:jid]),
	node: "http://jabber.org/protocol/commands"
) do |iq|
	StatsD.increment("command_list")

	reply = iq.reply
	reply.node = "http://jabber.org/protocol/commands"

	CustomerRepo.new(
		sgx_repo: Bwmsgsv2Repo.new,
		set_user: Sentry.method(:set_user)
	).find_by_jid(
		iq.from.stripped
	).catch {
		nil
	}.then { |customer|
		CommandList.for(customer, iq.from)
	}.then { |list|
		reply.items = list.map { |item|
			Blather::Stanza::DiscoItems::Item.new(
				iq.to,
				item[:node],
				item[:name]
			)
		}
		self << reply
	}
end

iq "/iq/ns:services", ns: "urn:xmpp:extdisco:2" do |iq|
	StatsD.increment("extdisco")

	reply = iq.reply
	reply << Nokogiri::XML::Builder.new {
		services(xmlns: "urn:xmpp:extdisco:2") do
			service(
				type: "sip",
				host: CONFIG[:sip_host]
			)
		end
	}.doc.root

	self << reply
end

Command.new(
	"jabber:iq:register",
	"Register",
	list_for: ->(*) { true },
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	google_play_userid = if Command.execution.iq.from.domain == "cheogram.com"
		Command.execution.iq.command.find(
			"./ns:userId", ns: "https://ns.cheogram.com/google-play"
		)&.first&.content
	end
	Command.customer.catch_only(CustomerRepo::NotFound) {
		Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Customer.create"))
		Command.execution.customer_repo.create(Command.execution.iq.from.stripped)
	}.then { |customer|
		Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Registration.for"))
		Registration.for(customer, google_play_userid, TEL_SELECTIONS).then(&:write)
	}.then {
		StatsD.increment("registration.completed")
	}.catch_only(Command::Execution::FinalStanza) do |e|
		StatsD.increment("registration.completed")
		EMPromise.reject(e)
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"info",
	"👤 Show Account Info",
	list_for: ->(*) { true },
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	Command.customer.then(&CustomerInfo.method(:for)).then do |info|
		Command.finish do |reply|
			reply.command << info.form
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"cdrs",
	"📲 Show Call Logs"
) {
	report_for = ((Date.today << 1)..Date.today)

	Command.customer.then { |customer|
		CDRRepo.new.find_range(customer, report_for)
	}.then do |cdrs|
		Command.finish do |reply|
			reply.command << FormTemplate.render("customer_cdr", cdrs: cdrs)
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"transactions",
	"🧾 Show Transactions",
	list_for: ->(customer:, **) { !!customer&.currency }
) {
	Command.customer.then(&:transactions).then do |txs|
		Command.finish do |reply|
			reply.command << FormTemplate.render("transactions", transactions: txs)
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"configure calls",
	"📞 Configure Calls",
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	Command.customer.then do |customer|
		cc_form = ConfigureCallsForm.new(customer)
		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << cc_form.render
		}.then { |iq|
			EMPromise.all(cc_form.parse(iq.form).map { |k, v|
				Command.execution.customer_repo.public_send("put_#{k}", customer, v)
			})
		}.then { Command.finish("Configuration saved!") }
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"ogm",
	"⏺️ Record Voicemail Greeting",
	list_for: ->(fwd: nil, **) { fwd&.voicemail_enabled? },
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	Command.customer.then do |customer|
		customer.fwd.create_call(CONFIG[:creds][:account]) do |cc|
			cc.from = customer.registered?.phone
			cc.application_id = CONFIG[:sip][:app]
			cc.answer_url = "#{CONFIG[:web_root]}/ogm/start?" \
			                "customer_id=#{customer.customer_id}"
		end
		Command.finish("You will now receive a call.")
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"migrate billing",
	"🏦 Switch to new billing",
	list_for: ->(tel:, customer:, **) { tel && !customer&.currency },
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	EMPromise.all([
		Command.customer,
		Command.reply do |reply|
			reply.allowed_actions = [:next]
			reply.command << FormTemplate.render("migrate_billing")
		end
	]).then do |(customer, iq)|
		plan_name = iq.form.field("plan_name").value.to_s
		customer = customer.with_plan(plan_name)
		customer.save_plan!.then {
			Registration::Payment.for(
				iq, customer, customer.registered?.phone,
				finish: PaypalDone
			)
		}.then(&:write).catch_only(Command::Execution::FinalStanza) do |s|
			BLATHER.join(CONFIG[:notify_admin], "sgx-jmp")
			BLATHER.say(
				CONFIG[:notify_admin],
				"#{customer.customer_id} migrated to #{customer.currency}",
				:groupchat
			)
			EMPromise.reject(s)
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"credit cards",
	"💳 Credit Card Settings and Management",
	list_for: ->(customer:, **) { !!customer&.currency }
) {
	Command.customer.then do |customer|
		url = CONFIG[:credit_card_url].call(
			customer.jid.to_s.gsub("\\", "%5C"),
			customer.customer_id
		)
		desc = "Manage credits cards and settings"
		Command.finish("#{desc}: #{url}") do |reply|
			oob = OOB.find_or_create(reply.command)
			oob.url = url
			oob.desc = desc
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"top up",
	"💲 Buy Account Credit by Credit Card",
	list_for: ->(payment_methods: [], **) { !payment_methods.empty? },
	format_error: ->(e) { "Failed to buy credit, system said: #{e.message}" }
) {
	Command.customer.then { |customer|
		BuyAccountCreditForm.for(customer).then do |credit_form|
			Command.reply { |reply|
				reply.allowed_actions = [:complete]
				reply.command << credit_form.form
			}.then do |iq|
				CreditCardSale.create(customer, **credit_form.parse(iq.form))
			end
		end
	}.then { |transaction|
		Command.finish("#{transaction} added to your account balance.")
	}.catch_only(
		AmountTooHighError,
		AmountTooLowError
	) do |e|
		Command.finish(e.message, type: :error)
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"alt top up",
	"🪙 Buy Account Credit by Bitcoin, Mail, or Interac e-Transfer",
	list_for: ->(customer:, **) { !!customer&.currency }
) {
	Command.customer.then { |customer|
		AltTopUpForm.for(customer)
	}.then do |alt_form|
		Command.reply { |reply|
			reply.allowed_actions = [:complete]
			reply.command << alt_form.form
		}.then do |iq|
			Command.finish { |reply| alt_form.parse(iq.form).action(reply) }
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"plan settings",
	"📝 Manage your plan, including overage limits",
	list_for: ->(customer:, **) { !!customer&.currency }
) {
	Command.customer.then { |customer|
		EMPromise.all([
			REDIS.get("jmp_customer_monthly_data_limit-#{customer.customer_id}"),
			SIMRepo.new.owned_by(customer)
		]).then { |(limit, sims)| [customer, sims, limit] }
	}.then do |(customer, sims, limit)|
		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << FormTemplate.render(
				"plan_settings", customer: customer, sims: sims, data_limit: limit
			)
		}.then { |iq|
			kwargs = {
				monthly_overage_limit: iq.form.field("monthly_overage_limit")&.value,
				monthly_data_limit: iq.form.field("monthly_data_limit")&.value
			}.compact
			Command.execution.customer_repo.put_monthly_limits(customer, **kwargs)
		}.then { Command.finish("Configuration saved!") }
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"referral codes",
	"👥 Refer a friend for free credit",
	list_for: ->(customer:, **) { !!customer&.currency }
) {
	repo = InvitesRepo.new
	Command.customer.then { |customer|
		EMPromise.all([
			repo.find_or_create_group_code(customer.customer_id),
			repo.unused_invites(customer.customer_id)
		])
	}.then do |(group_code, invites)|
		if invites.empty?
			Command.finish(
				"This code will provide credit equivalent to one month of service " \
				"to anyone after they sign up and pay: #{group_code}\n\n" \
				"You will receive credit equivalent to one month of service once " \
				"their payment clears."
			)
		else
			Command.finish do |reply|
				reply.command << FormTemplate.render(
					"codes",
					invites: invites,
					group_code: group_code
				)
			end
		end
	end
}.register(self).then(&CommandList.method(:register))

# Assumes notify_from is a direct target
notify_to = CONFIG[:direct_targets].fetch(
	Blather::JID.new(CONFIG[:notify_from]).node.to_sym
)

Command.new(
	"sims",
	"📶 (e)SIM Details",
	list_for: ->(customer:, **) { CONFIG[:keepgo] && !!customer&.currency },
	customer_repo: CustomerRepo.new(
		sgx_repo: TrivialBackendSgxRepo.new(jid: notify_to)
	)
) {
	Command.customer.then { |customer|
		EMPromise.all([customer, SIMRepo.new.owned_by(customer)])
	}.then do |(customer, sims)|
		Command.reply { |reply|
			reply.command << FormTemplate.render("sim_details", sims: sims)
		}.then { |iq|
			case iq.form.field("http://jabber.org/protocol/commands#actions")&.value
			when "order-sim"
				SIMOrder.for(customer, **CONFIG.dig(:sims, :sim, customer.currency))
			when "order-esim"
				SIMOrder::ESIM.for(
					customer, **CONFIG.dig(:sims, :esim, customer.currency)
				)
			when "edit-nicknames"
				EditSimNicknames.new(customer, sims)
			else
				Command.finish
			end
		}.then { |action|
			Command.reply { |reply|
				reply.allowed_actions = [:complete]
				reply.command << action.form
			}.then(&action.method(:complete))
		}
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"subaccount",
	"➕️ Create a new phone number linked to this balance",
	list_for: lambda do |customer:, **|
		!!customer&.currency &&
		customer&.billing_customer_id == customer&.customer_id
	end
) {
	cheogram = Command.execution.iq.from.resource =~ /\ACheogram/
	Command.customer.then do |customer|
		ParentCodeRepo.new.find_or_create(customer.customer_id).then do |code|
			Command.finish { |reply|
				reply.command << FormTemplate.render(
					"subaccount", code: code, cheogram: cheogram
				)
			}
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"reset sip account",
	"☎️ Create or Reset SIP Account",
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	Command.customer.then do |customer|
		sip_account = customer.reset_sip_account
		Command.reply { |reply|
			reply.allowed_actions = [:next]
			form = sip_account.form
			form.type = :form
			form.fields += [{
				type: :boolean, var: "change_fwd",
				label: "Should inbound calls forward to this SIP account?"
			}]
			reply.command << form
		}.then do |fwd|
			if ["1", "true"].include?(fwd.form.field("change_fwd")&.value.to_s)
				Command.execution.customer_repo.put_fwd(
					customer,
					customer.fwd.with(uri: sip_account.uri)
				).then { Command.finish("Inbound calls will now forward to SIP.") }
			else
				Command.finish
			end
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"lnp",
	"#️⃣ Port in your number from another carrier",
	list_for: ->(**) { true },
	customer_repo: CustomerRepo.new(
		sgx_repo: TrivialBackendSgxRepo.new(jid: notify_to)
	)
) {
	Command.customer.then do |customer|
		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << FormTemplate.render("lnp")
		}.then { |iq|
			PortInOrder.parse(customer, iq.form).complete_with do |form|
				Command.reply { |reply|
					reply.allowed_actions = [:next]
					reply.command << form
				}.then(&:form)
			end
		}.then do |order|
			unless order.already_inservice?
				order_id = BandwidthIris::PortIn.create(order.to_h)[:order_id]
				customer.stanza_from(Blather::Stanza::Message.new(
					"",
					order.message(order_id)
				))
				Command.finish(
					"Your port-in request has been accepted, " \
					"support will contact you with next steps"
				)
			end
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"terminate account",
	"❌ Cancel your account and terminate your phone number",
	list_for: ->(**) { false },
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	Command.reply { |reply|
		reply.allowed_actions = [:next]
		reply.note_text = "Press next to confirm your account termination."
	}.then { Command.customer }.then { |customer|
		AdminAction::CancelCustomer.call(
			customer,
			customer_repo: Command.execution.customer_repo
		)
	}.then do
		Command.finish("Account cancelled")
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"customer info",
	"Show Customer Info",
	list_for: ->(customer: nil, **) { customer&.admin? }
) {
	Command.customer.then do |customer|
		raise AuthError, "You are not an admin" unless customer&.admin?

		customer_repo = CustomerRepo.new(
			sgx_repo: Bwmsgsv2Repo.new,
			bandwidth_tn_repo: EmptyRepo.new(BandwidthTnRepo.new) # No CNAM in admin
		)

		AdminCommand::NoUser.new(customer_repo).start
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"reachability",
	"Test Reachability",
	list_for: ->(customer: nil, **) { customer&.admin? }
) {
	Command.customer.then do |customer|
		raise AuthError, "You are not an admin" unless customer&.admin?

		form = ReachabilityForm.new(CustomerRepo.new)

		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << form.render
		}.then { |response|
			form.parse(response.form)
		}.then { |result|
			result.repo.get_or_create(result.target).then { |v|
				result.target.stanza_from(result.prompt) if result.prompt

				Command.finish { |reply|
					reply.command << form.render_result(v)
				}
			}
		}.catch_only(RuntimeError) { |e|
			Command.finish(e, type: :error)
		}
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"snikket",
	"Launch Snikket Instance",
	list_for: ->(customer: nil, **) { customer&.admin? }
) {
	Command.customer.then do |customer|
		raise AuthError, "You are not an admin" unless customer&.admin?

		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << FormTemplate.render("snikket_launch")
		}.then { |response|
			domain = response.form.field("domain").value.to_s
			test_instance = response.field("test_instance")&.value.to_s
			IQ_MANAGER.write(Snikket::Launch.new(
				nil, CONFIG[:snikket_hosting_api],
				domain: domain, test_instance: test_instance
			)).then do |launched|
				Snikket::CustomerInstance.for(customer, domain, launched)
			end
		}.then { |instance|
			Command.finish do |reply|
				reply.command << FormTemplate.render(
					"snikket_launched",
					instance: instance
				)
			end
		}
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"stop snikket",
	"STOP Snikket Instance",
	list_for: ->(customer: nil, **) { customer&.admin? }
) {
	Command.customer.then do |customer|
		raise AuthError, "You are not an admin" unless customer&.admin?

		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << FormTemplate.render("snikket_stop")
		}.then { |response|
			instance_id = response.form.field("instance_id").value.to_s
			IQ_MANAGER.write(Snikket::Stop.new(
				nil, CONFIG[:snikket_hosting_api],
				instance_id: instance_id
			))
		}.then { |iq|
			Command.finish(iq.to_s)
		}
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"delete snikket",
	"DELETE Snikket Instance",
	list_for: ->(customer: nil, **) { customer&.admin? }
) {
	Command.customer.then do |customer|
		raise AuthError, "You are not an admin" unless customer&.admin?

		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << FormTemplate.render("snikket_delete")
		}.then { |response|
			instance_id = response.form.field("instance_id").value.to_s
			IQ_MANAGER.write(Snikket::Delete.new(
				nil, CONFIG[:snikket_hosting_api],
				instance_id: instance_id
			))
		}.then { |iq|
			Command.finish(iq.to_s)
		}
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"find snikket",
	"Lookup Snikket Instance",
	list_for: ->(customer: nil, **) { customer&.admin? }
) {
	Command.customer.then do |customer|
		raise AuthError, "You are not an admin" unless customer&.admin?

		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << FormTemplate.render("snikket_launch")
		}.then { |response|
			domain = response.form.field("domain").value.to_s
			IQ_MANAGER.write(Snikket::DomainInfo.new(
				nil, CONFIG[:snikket_hosting_api],
				domain: domain
			))
		}.then { |instance|
			Command.finish do |reply|
				reply.command << FormTemplate.render(
					"snikket_result",
					instance: instance
				)
			end
		}
	end
}.register(self).then(&CommandList.method(:register))

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.new(
	"https://ns.cheogram.com/sgx/jid-switch",
	"Change JID",
	list_for: lambda { |customer: nil, from_jid: nil, **|
		customer || from_jid.to_s =~ Regexp.new(CONFIG[:onboarding_domain])
	},
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	Command.customer.then { |customer|
		Command.reply { |reply|
			reply.command << FormTemplate.render("jid_switch")
		}.then { |response|
			new_jid = response.form.field("jid").value
			repo = Command.execution.customer_repo
			repo.find_by_jid(new_jid).catch_only(CustomerRepo::NotFound) { nil }
				.then { |cust|
					next EMPromise.reject("Customer Already Exists") if cust

					repo.change_jid(customer, new_jid)
				}
		}.then {
			StatsD.increment("changejid.completed")
			jid = ProxiedJID.new(customer.jid).unproxied
			if jid.domain == CONFIG[:onboarding_domain]
				CustomerRepo.new.find(customer.customer_id).then do |cust|
					WelcomeMessage.new(cust, customer.registered?.phone).welcome
				end
			end
			Command.finish { |reply|
				reply.note_type = :info
				reply.note_text = "Customer JID Changed"
			}
		}
	}
}.register(self).then(&CommandList.method(:register))

Command.new(
	"web-register",
	"Initiate Register from Web",
	list_for: lambda { |from_jid: nil, **|
		from_jid&.stripped.to_s == CONFIG[:web_register][:from]
	}
) {
	if Command.execution.iq.from.stripped != CONFIG[:web_register][:from]
		next EMPromise.reject(
			Command::Execution::FinalStanza.new(iq.as_error("forbidden", :auth))
		)
	end

	Command.reply { |reply|
		reply.command << FormTemplate.render("web_register")
	}.then do |iq|
		jid = iq.form.field("jid")&.value.to_s.strip
		tel = iq.form.field("tel")&.value.to_s.strip
		if jid !~ /\./ || jid =~ /\s/
			Command.finish("The Jabber ID you entered was not valid.", type: :error)
		elsif tel !~ /\A\+\d+\Z/
			Command.finish("Invalid telephone number", type: :error)
		else
			IQ_MANAGER.write(Blather::Stanza::Iq::Command.new.tap { |cmd|
				cmd.to = CONFIG[:web_register][:to]
				cmd.node = "push-register"
				cmd.form.fields = [{ var: "to", value: jid }]
				cmd.form.type = "submit"
			}).then { |result|
				TEL_SELECTIONS.set_tel(result.form.field("from")&.value.to_s.strip, tel)
			}.then { Command.finish }
		end
	end
}.register(self).then(&CommandList.method(:register))

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

iq type: [:result, :error] do |iq|
	IQ_MANAGER.fulfill(iq)
	true
end

iq type: [:get, :set] do |iq|
	StatsD.increment("unknown_iq")

	self << Blather::StanzaError.new(iq, "feature-not-implemented", :cancel)
end

trap(:INT) { EM.stop }
trap(:TERM) { EM.stop }
EM.run { client.run }
