# frozen_string_literal: true

require "erb"
require "ruby-bandwidth-iris"
require "securerandom"

require_relative "./alt_top_up_form"
require_relative "./command"
require_relative "./bandwidth_tn_order"
require_relative "./em"
require_relative "./oob"
require_relative "./web_register_manager"

class Registration
	def self.for(customer, web_register_manager)
		customer.registered?.then do |registered|
			if registered
				Registered.new(registered.phone)
			else
				web_register_manager[customer.jid].choose_tel.then do |tel|
					Activation.for(customer, tel)
				end
			end
		end
	end

	class Registered
		def initialize(tel)
			@tel = tel
		end

		def write
			Command.finish("You are already registered with JMP number #{@tel}")
		end
	end

	class Activation
		def self.for(customer, tel)
			if customer.active?
				Finish.new(customer, tel)
			else
				EMPromise.resolve(new(customer, tel))
			end
		end

		def initialize(customer, tel)
			@customer = customer
			@tel = tel
		end

		attr_reader :customer, :tel

		FORM_FIELDS = [
			{
				var: "activation_method",
				type: "list-single",
				label: "Activate using",
				required: true,
				options: [
					{
						value: "credit_card",
						label: "Credit Card"
					},
					{
						value: "bitcoin",
						label: "Bitcoin"
					},
					{
						value: "code",
						label: "Invite Code"
					},
					{
						value: "mail",
						label: "Mail or eTransfer"
					}
				]
			},
			{
				var: "plan_name",
				type: "list-single",
				label: "What currency should your account balance be in?",
				required: true,
				options: [
					{
						value: "cad_beta_unlimited-v20210223",
						label: "Canadian Dollars"
					},
					{
						value: "usd_beta_unlimited-v20210223",
						label: "United States Dollars"
					}
				]
			}
		].freeze

		ACTIVATE_INSTRUCTION =
			"To activate your account, you can either deposit " \
			"$#{CONFIG[:activation_amount]} to your balance or enter " \
			"your invite code if you have one."

		CRYPTOCURRENCY_INSTRUCTION =
			"(If you'd like to pay in a cryptocurrency other than " \
			"Bitcoin, currently we recommend using a service like " \
			"simpleswap.io, morphtoken.com, changenow.io, or godex.io. " \
			"Manual payment via Bitcoin Cash is also available if you " \
			"contact support.)"

		def add_instructions(form, center)
			center = " (#{center})" if center
			[
				"You've selected #{tel}#{center} as your JMP number",
				ACTIVATE_INSTRUCTION,
				CRYPTOCURRENCY_INSTRUCTION
			].each do |txt|
				form << Blather::XMPPNode.new(:instructions, form.document).tap do |i|
					i << txt
				end
			end
		end

		def write
			rate_center.then { |center|
				Command.reply do |reply|
					reply.allowed_actions = [:next]
					form = reply.form
					form.type = :form
					form.title = "Activate JMP"
					add_instructions(form, center)
					form.fields = FORM_FIELDS
				end
			}.then { |iq| Payment.for(iq, customer, tel) }.then(&:write)
		end

	protected

		def rate_center
			EM.promise_fiber {
				center = BandwidthIris::Tn.get(tel).get_rate_center
				"#{center[:rate_center]}, #{center[:state]}"
			}.catch { nil }
		end
	end

	module Payment
		def self.kinds
			@kinds ||= {}
		end

		def self.for(iq, customer, tel)
			plan_name = iq.form.field("plan_name").value.to_s
			customer = customer.with_plan(plan_name)
			kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
				raise "Invalid activation method"
			}.call(customer, tel)
		end

		class Bitcoin
			Payment.kinds[:bitcoin] = method(:new)

			def initialize(customer, tel)
				@customer = customer
				@customer_id = customer.customer_id
				@tel = tel
			end

			attr_reader :customer_id, :tel

			def legacy_session_save
				sid = SecureRandom.hex
				REDIS.mset(
					"reg-sid_for-#{customer_id}", sid,
					"reg-session_tel-#{sid}", tel
				)
			end

			def save
				EMPromise.all([
					legacy_session_save,
					REDIS.mset(
						"pending_tel_for-#{customer_id}", tel,
						"pending_plan_for-#{customer_id}", @customer.plan_name
					)
				])
			end

			def note_text(amount, addr)
				<<~NOTE
					Activate your account by sending at least #{'%.6f' % amount} BTC to
					#{addr}

					You will receive a notification when your payment is complete.
				NOTE
			end

			def write
				EMPromise.all([
					addr,
					save,
					BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
				]).then do |(addr, _, rate)|
					min = CONFIG[:activation_amount] / rate
					Command.finish(note_text(min, addr), status: :canceled)
				end
			end

		protected

			def addr
				@addr ||= @customer.btc_addresses.then do |addrs|
					addrs.first || @customer.add_btc_address
				end
			end
		end

		class CreditCard
			Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }

			def self.for(customer, tel)
				customer.payment_methods.then do |payment_methods|
					if (method = payment_methods.default_payment_method)
						Activate.new(customer, method, tel)
					else
						new(customer, tel)
					end
				end
			end

			def initialize(customer, tel)
				@customer = customer
				@tel = tel
			end

			def oob(reply)
				oob = OOB.find_or_create(reply.command)
				oob.url = CONFIG[:credit_card_url].call(
					reply.to.stripped.to_s.gsub("\\", "%5C"),
					@customer.customer_id
				)
				oob.desc = "Add credit card, then return here to continue"
				oob
			end

			def write
				Command.reply { |reply|
					reply.allowed_actions = [:next]
					reply.note_type = :info
					reply.note_text = "#{oob(reply).desc}: #{oob(reply).url}"
				}.then do
					CreditCard.for(@customer, @tel).then(&:write)
				end
			end

			class Activate
				def initialize(customer, payment_method, tel)
					@customer = customer
					@payment_method = payment_method
					@tel = tel
				end

				def write
					Transaction.sale(
						@customer,
						amount: CONFIG[:activation_amount],
						payment_method: @payment_method
					).then(
						method(:sold),
						->(_) { declined }
					)
				end

			protected

				def sold(tx)
					tx.insert.then {
						@customer.bill_plan
					}.then do
						Finish.new(@customer, @tel).write
					end
				end

				DECLINE_MESSAGE =
					"Your bank declined the transaction. " \
					"Often this happens when a person's credit card " \
					"is a US card that does not support international " \
					"transactions, as JMP is not based in the USA, though " \
					"we do support transactions in USD.\n\n" \
					"If you were trying a prepaid card, you may wish to use "\
					"Privacy.com instead, as they do support international " \
					"transactions.\n\n " \
					"You may add another card and then return here"

				def decline_oob(reply)
					oob = OOB.find_or_create(reply.command)
					oob.url = CONFIG[:credit_card_url].call(
						reply.to.stripped.to_s.gsub("\\", "%5C"),
						@customer.customer_id
					)
					oob.desc = DECLINE_MESSAGE
					oob
				end

				def declined
					Command.reply { |reply|
						reply_oob = decline_oob(reply)
						reply.allowed_actions = [:next]
						reply.note_type = :error
						reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
					}.then do
						CreditCard.for(@customer, @tel).then(&:write)
					end
				end
			end
		end

		class InviteCode
			Payment.kinds[:code] = method(:new)

			class Invalid < StandardError; end

			FIELDS = [{
				var: "code",
				type: "text-single",
				label: "Your invite code",
				required: true
			}].freeze

			def initialize(customer, tel, error: nil)
				@customer = customer
				@tel = tel
				@error = error
			end

			def add_form(reply)
				form = reply.form
				form.type = :form
				form.title = "Enter Invite Code"
				form.instructions = @error if @error
				form.fields = FIELDS
			end

			def write
				Command.reply { |reply|
					reply.allowed_actions = [:next]
					add_form(reply)
				}.then(&method(:parse))
			end

			def parse(iq)
				guard_too_many_tries.then {
					verify(iq.form.field("code")&.value&.to_s)
				}.then {
					Finish.new(@customer, @tel)
				}.catch_only(Invalid, &method(:invalid_code)).then(&:write)
			end

		protected

			def guard_too_many_tries
				REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
					raise Invalid, "Too many wrong attempts" if t.to_i > 10
				end
			end

			def invalid_code(e)
				EMPromise.all([
					REDIS.incr("jmp_invite_tries-#{customer_id}").then do
						REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
					end,
					InviteCode.new(@customer, @tel, error: e.message)
				]).then(&:last)
			end

			def customer_id
				@customer.customer_id
			end

			def verify(code)
				EMPromise.resolve(nil).then do
					DB.transaction do
						valid = DB.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive?
							UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP
							WHERE code=$2 AND used_by_id IS NULL
						SQL
						raise Invalid, "Not a valid invite code: #{code}" unless valid
						@customer.activate_plan_starting_now
					end
				end
			end
		end

		class Mail
			Payment.kinds[:mail] = method(:new)

			def initialize(_customer, _tel); end

			def form
				form = Blather::Stanza::X.new(:result)
				form.title = "Activate by Mail or eTransfer"
				form.instructions =
					"Activate your account by sending at least " \
					"$#{CONFIG[:activation_amount]}\nWe support payment by " \
					"postal mail or, in Canada, by Interac eTransfer.\n\n" \
					"You will receive a notification when your payment is complete."

				form.fields = fields.to_a
				form
			end

			def fields
				[
					AltTopUpForm::MAILING_ADDRESS,
					AltTopUpForm::IS_CAD
				].flatten
			end

			def write
				Command.finish(status: :canceled) do |reply|
					reply.command << form
				end
			end
		end
	end

	class Finish
		def initialize(customer, tel)
			@customer = customer
			@tel = tel
		end

		def write
			BandwidthTNOrder.create(@tel).then(&:poll).then(
				->(_) { customer_active_tel_purchased },
				lambda do |_|
					Command.finish(
						"The JMP number #{@tel} is no longer available, " \
						"please visit https://jmp.chat and choose another.",
						type: :error
					)
				end
			)
		end

	protected

		def cheogram_sip_addr
			"sip:#{ERB::Util.url_encode(@customer.jid)}@sip.cheogram.com"
		end

		def raise_setup_error(e)
			Command.log.error "@customer.register! failed", e
			Command.finish(
				"There was an error setting up your number, " \
				"please contact JMP support.",
				type: :error
			)
		end

		def customer_active_tel_purchased
			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
				EMPromise.all([
					REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
					@customer.fwd_timeout = 25 # ~5 seconds / ring, 5 rings
				])
			}.then do
				Command.finish("Your JMP account has been activated as #{@tel}")
			end
		end
	end
end
