# frozen_string_literal: true

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

require_relative "./alt_top_up_form"
require_relative "./bandwidth_tn_order"
require_relative "./bandwidth_tn_reservation_repo"
require_relative "./command"
require_relative "./em"
require_relative "./invites_repo"
require_relative "./oob"
require_relative "./parent_code_repo"
require_relative "./proxied_jid"
require_relative "./tel_selections"
require_relative "./welcome_message"

class Registration
	def self.for(customer, google_play_userid, tel_selections)
		if (reg = customer.registered?)
			Registered.for(customer, reg.phone)
		else
			tel_selections[customer.jid].then(&:choose_tel).then do |tel|
				reserve_and_continue(tel_selections, customer, tel).then do
					FinishOrStartActivation.for(customer, google_play_userid, tel)
				end
			end
		end
	end

	def self.reserve_and_continue(tel_selections, customer, tel)
		tel.reserve(customer).catch do
			tel_selections.delete(customer.jid).then {
				tel_selections[customer.jid]
			}.then { |choose|
				choose.choose_tel(
					error: "The JMP number #{tel} is no longer available."
				)
			}.then { |n_tel| reserve_and_continue(tel_selections, customer, n_tel) }
		end
	end

	class Registered
		def self.for(customer, tel)
			jid = ProxiedJID.new(customer.jid).unproxied
			if jid.domain == CONFIG[:onboarding_domain]
				FinishOnboarding.for(customer, tel)
			else
				new(tel)
			end
		end

		def initialize(tel)
			@tel = tel
		end

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

	class FinishOrStartActivation
		def self.for(customer, google_play_userid, tel)
			if customer.active?
				Finish.new(customer, tel)
			elsif customer.balance >= CONFIG[:activation_amount_accept]
				BillPlan.new(customer, tel)
			else
				new(customer, google_play_userid, tel)
			end
		end

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

		def write
			Command.reply { |reply|
				reply.allowed_actions = [:next]
				reply.note_type = :info
				reply.note_text = File.read("#{__dir__}/../fup.txt")
			}.then { Activation.for(@customer, @google_play_userid, @tel).write }
		end
	end

	class Activation
		def self.for(customer, google_play_userid, tel)
			jid = ProxiedJID.new(customer.jid).unproxied
			if CONFIG[:approved_domains].key?(jid.domain.to_sym)
				Allow.for(customer, tel, jid)
			elsif google_play_userid
				GooglePlay.new(customer, google_play_userid, tel)
			else
				new(customer, tel)
			end
		end

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

		attr_reader :customer, :tel

		def form
			FormTemplate.render("registration/activate", tel: tel)
		end

		def write
			Command.reply { |reply|
				reply.allowed_actions = [:next]
				reply.command << form
			}.then(&method(:next_step))
		end

		def next_step(iq)
			EMPromise.resolve(nil).then do
				plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
				@customer = @customer.with_plan(plan.name)
				Registration::Payment::InviteCode.new(
					@customer, @tel, finish: Finish, db: DB, redis: REDIS
				).parse(iq, force_save_plan: true)
					.catch_only(InvitesRepo::Invalid) do
						Payment.for(iq, @customer, @tel).then(&:write)
					end
			end
		end

		class GooglePlay
			def initialize(customer, google_play_userid, tel)
				@customer = customer
				@google_play_userid = google_play_userid
				@tel = tel
				@invites = InvitesRepo.new(DB, REDIS)
				@parent_code_repo = ParentCodeRepo.new(redis: REDIS, db: DB)
			end

			def used
				REDIS.sismember("google_play_userids", @google_play_userid)
			end

			def form
				FormTemplate.render(
					"registration/google_play",
					tel: @tel
				)
			end

			def write
				used.then do |u|
					next Activation.for(@customer, nil, @tel).write if u.to_s == "1"

					Command.reply { |reply|
						reply.allowed_actions = [:next]
						reply.command << form
					}.then(&method(:activate)).then do
						Finish.new(@customer, @tel).write
					end
				end
			end

			def activate(iq)
				plan = Plan.for_registration(iq.form.field("plan_name").value)
				code = iq.form.field("code")&.value
				EMPromise.all([
					@parent_code_repo.find(code),
					REDIS.sadd("google_play_userids", @google_play_userid)
				]).then { |(parent, _)|
					save_active_plan(plan, parent)
				}.then do
					use_referral_code(code)
				end
			end

		protected

			def save_bogus_transaction
				Transaction.new(
					customer_id: @customer.customer_id,
					transaction_id: "google_play_#{@customer.customer_id}",
					amount: 0,
					note: "Activated via Google Play",
					bonus_eligible?: false
				).insert
			end

			def save_active_plan(plan, parent)
				@customer = @customer.with_plan(plan.name, parent_customer_id: parent)
				save_bogus_transaction.then do
					@customer.activate_plan_starting_now
				end
			end

			def use_referral_code(code)
				EMPromise.resolve(nil).then {
					@invites.claim_code(@customer.customer_id, code) {
						@customer.extend_plan
					}
				}.catch_only(InvitesRepo::Invalid) do
					@invites.stash_code(@customer.customer_id, code)
				end
			end
		end

		class Allow < Activation
			def self.for(customer, tel, jid)
				credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
				new(customer, tel, credit_to)
			end

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

			def form
				FormTemplate.render(
					"registration/allow",
					tel: tel,
					domain: customer.jid.domain
				)
			end

			def next_step(iq)
				plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
				@customer = customer.with_plan(plan.name)
				EMPromise.resolve(nil).then { activate }.then do
					Finish.new(customer, tel).write
				end
			end

		protected

			def activate
				DB.transaction do
					if @credit_to
						InvitesRepo.new(DB, REDIS).create_claimed_code(
							@credit_to,
							customer.customer_id
						)
					end
					@customer.activate_plan_starting_now
				end
			end
		end
	end

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

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

		class CryptoPaymentMethod
			def crypto_addrs
				raise NotImplementedError, "Subclass must implement"
			end

			def reg_form_name
				raise NotImplementedError, "Subclass must implement"
			end

			def sell_prices
				raise NotImplementedError, "Subclass must implement"
			end

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

			def save
				TEL_SELECTIONS.set(@customer.jid, @tel)
			end

			attr_reader :customer_id, :tel

			def form(rate, addr)
				amount = CONFIG[:activation_amount] / rate

				FormTemplate.render(
					reg_form_name,
					amount: amount,
					addr: addr
				)
			end

			def write
				EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
					Command.reply { |reply|
						reply.allowed_actions = [:prev]
						reply.status = :canceled
						reply.command << form(rate, addr)
					}.then(&method(:handle_possible_prev))
				end
			end

		protected

			def handle_possible_prev(iq)
				raise "Action not allowed" unless iq.prev?

				Activation.for(@customer, nil, @tel).then(&:write)
			end

			def addr_and_rate
				EMPromise.all([
					crypto_addrs.then { |addrs|
						addrs.first || add_crypto_addr
					},

					sell_prices.public_send(@customer.currency.to_s.downcase)
				])
			end
		end

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

			def reg_form_name
				"registration/btc"
			end

			def sell_prices
				BTC_SELL_PRICES
			end

			def crypto_addrs
				@customer.btc_addresses
			end

			def add_crypto_addr
				@customer.add_btc_address
			end
		end

		## Like Bitcoin
		class BCH < CryptoPaymentMethod
			Payment.kinds[:bch] = method(:new)

			def reg_form_name
				"registration/bch"
			end

			def sell_prices
				BCH_SELL_PRICES
			end

			def crypto_addrs
				@customer.bch_addresses
			end

			def add_crypto_addr
				@customer.add_bch_address
			end
		end

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

			def self.for(in_customer, tel, finish: Finish, **)
				reload_customer(in_customer).then do |customer|
					if customer.balance >= CONFIG[:activation_amount_accept]
						next BillPlan.new(customer, tel, finish: finish)
					end

					new(customer, tel, finish: finish)
				end
			end

			def self.reload_customer(customer)
				EMPromise.resolve(nil).then do
					Command.execution.customer_repo.find(customer.customer_id)
				end
			end

			def initialize(customer, tel, finish: Finish)
				@customer = customer
				@tel = tel
				@finish = finish
			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
				) + "&amount=#{CONFIG[:activation_amount]}"
				oob.desc = "Pay by credit card, save, then next here to continue"
				oob
			end

			def write
				Command.reply { |reply|
					reply.allowed_actions = [:next, :prev]
					toob = oob(reply)
					reply.note_type = :info
					reply.note_text = "#{toob.desc}: #{toob.url}"
				}.then do |iq|
					next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?

					CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
				end
			end
		end

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

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

			def initialize(
				customer, tel,
				error: nil, finish: Finish, db: DB, redis: REDIS, **
			)
				@customer = customer
				@tel = tel
				@error = error
				@finish = finish
				@invites_repo = InvitesRepo.new(db, redis)
				@parent_code_repo = ParentCodeRepo.new(db: db, redis: redis)
			end

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

			def write
				Command.reply { |reply|
					reply.allowed_actions = [:next, :prev]
					add_form(reply)
				}.then(&method(:parse)).catch_only(InvitesRepo::Invalid) { |e|
					invalid_code(e).write
				}
			end

			def parse(iq, force_save_plan: false)
				return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?

				verify(iq.form.field("code")&.value&.to_s, force_save_plan)
					.then(&:write)
			end

		protected

			def invalid_code(e)
				self.class.new(@customer, @tel, error: e.message, finish: @finish)
			end

			def customer_id
				@customer.customer_id
			end

			def verify(code, force_save_plan)
				@parent_code_repo.claim_code(@customer, code) {
					check_parent_balance
				}.catch_only(ParentCodeRepo::Invalid) {
					(@customer.save_plan! if force_save_plan).then do
						@invites_repo.claim_code(customer_id, code) {
							@customer.activate_plan_starting_now
						}.then { @finish.new(@customer, @tel) }
					end
				}
			end

			def reload_customer
				Command.execution.customer_repo.find(@customer.customer_id)
			end

			def check_parent_balance
				reload_customer.then do |customer|
					if customer.balance >= CONFIG[:activation_amount_accept]
						next BillPlan.new(customer, @tel, finish: @finish)
					end

					msg = "Account balance not enough to cover the activation"
					invalid_code(RuntimeError.new(msg))
				end
			end
		end

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

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

			def form
				FormTemplate.render(
					"registration/mail",
					currency: @customer.currency,
					**onboarding_extras
				)
			end

			def onboarding_extras
				jid = ProxiedJID.new(@customer.jid).unproxied
				return {} unless jid.domain == CONFIG[:onboarding_domain]

				{
					customer_id: @customer.customer_id,
					in_note: "Customer ID"
				}
			end

			def write
				Command.reply { |reply|
					reply.allowed_actions = [:prev]
					reply.status = :canceled
					reply.command << form
				}.then { |iq|
					raise "Action not allowed" unless iq.prev?

					Activation.for(@customer, nil, @tel).then(&:write)
				}
			end
		end
	end

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

		def write
			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
				@finish.new(@customer, @tel).write
			end
		end
	end

	class Finish
		def initialize(customer, tel)
			@customer = customer
			@tel = tel
			@invites = InvitesRepo.new(DB, REDIS)
		end

		def write
			@tel.order(DB, @customer).then(
				->(_) { customer_active_tel_purchased },
				method(:number_purchase_error)
			)
		end

	protected

		def number_purchase_error(e)
			Command.log.error "number_purchase_error", e
			TEL_SELECTIONS.delete(@customer.jid).then {
				TEL_SELECTIONS[@customer.jid]
			}.then { |choose|
				choose.choose_tel(
					error: "The JMP number #{@tel} is no longer available."
				)
			}.then { |tel| Finish.new(@customer, tel).write }
		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 put_default_fwd
			Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel.tel, CustomerFwd.for(
				uri: "xmpp:#{@customer.jid}",
				voicemail_enabled: true
			))
		end

		def use_referral_code
			@invites.use_pending_group_code(@customer.customer_id).then do |credit_to|
				next unless credit_to

				Transaction.new(
					customer_id: @customer.customer_id,
					transaction_id: "referral_#{@customer.customer_id}_#{credit_to}",
					amount: @customer.monthly_price,
					note: "Referral Bonus",
					bonus_eligible?: false
				).insert
			end
		end

		def customer_active_tel_purchased
			@customer.register!(@tel.tel).catch(&method(:raise_setup_error)).then {
				EMPromise.all([
					TEL_SELECTIONS.delete(@customer.jid),
					put_default_fwd,
					use_referral_code
				])
			}.then do
				FinishOnboarding.for(@customer, @tel).then(&:write)
			end
		end
	end

	module FinishOnboarding
		def self.for(customer, tel, db: LazyObject.new { DB })
			jid = ProxiedJID.new(customer.jid).unproxied
			if jid.domain == CONFIG[:onboarding_domain]
				Snikket.for(customer, tel, db: db)
			else
				NotOnboarding.new(customer, tel)
			end
		end

		class Snikket
			def self.for(customer, tel, db:)
				::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
					if is.empty?
						new(customer, tel, db: db)
					elsif is[0].bootstrap_token.empty?
						# This is a need_dns one, try the launch again
						new(customer, tel, db: db).launch(is[0].domain)
					else
						GetInvite.for(customer, is[0], tel, db: db)
					end
				end
			end

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

			ACTION_VAR = "http://jabber.org/protocol/commands#actions"

			def form
				FormTemplate.render(
					"registration/snikket",
					tel: @tel,
					error: @error
				)
			end

			def write
				Command.reply { |reply|
					reply.allowed_actions = [:next]
					reply.command << form
				}.then(&method(:next_step))
			end

			def next_step(iq)
				subdomain = empty_nil(iq.form.field("subdomain")&.value)
				domain = "#{subdomain}.snikket.chat"
				if iq.form.field(ACTION_VAR)&.value == "custom_domain"
					CustomDomain.new(@customer, @tel, old: @old).write
				elsif @old && (!subdomain || domain == @old.domain)
					GetInvite.for(@customer, @old, @tel, db: @db).then(&:write)
				else
					launch(domain)
				end
			end

			def launch(domain)
				IQ_MANAGER.write(::Snikket::Launch.new(
					nil, CONFIG[:snikket_hosting_api], domain: domain
				)).then { |launched|
					save_instance_and_wait(domain, launched)
				}.catch { |e|
					next EMPromise.reject(e) unless e.respond_to?(:text)

					Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
				}
			end

			def save_instance_and_wait(domain, launched)
				instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
				repo = ::Snikket::Repo.new(db: @db)
				(@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old))
					.then { repo.put(instance) }.then do
						if launched.status == :needs_dns
							NeedsDNS.new(@customer, instance, @tel, launched.records).write
						else
							GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
						end
					end
			end

			def empty_nil(s)
				s.nil? || s.empty? ? nil : s
			end

			class NeedsDNS < Snikket
				def initialize(customer, instance, tel, records, db: DB)
					@customer = customer
					@instance = instance
					@tel = tel
					@records = records
					@db = db
				end

				def form
					FormTemplate.render(
						"registration/snikket_needs_dns",
						records: @records
					)
				end

				def write
					Command.reply { |reply|
						reply.allowed_actions = [:prev, :next]
						reply.command << form
					}.then do |iq|
						if iq.prev?
							CustomDomain.new(@customer, @tel, old: @instance).write
						else
							launch(@instance.domain)
						end
					end
				end
			end

			class GetInvite
				def self.for(customer, instance, tel, db: DB)
					instance.fetch_invite.then do |xmpp_uri|
						if xmpp_uri
							GoToInvite.new(xmpp_uri)
						else
							new(customer, instance, tel, db: db)
						end
					end
				end

				def initialize(customer, instance, tel, db: DB)
					@customer = customer
					@instance = instance
					@tel = tel
					@db = db
				end

				def form
					FormTemplate.render(
						"registration/snikket_wait",
						domain: @instance.domain
					)
				end

				def write
					Command.reply { |reply|
						reply.allowed_actions = [:prev, :next]
						reply.command << form
					}.then do |iq|
						if iq.prev?
							Snikket.new(@customer, @tel, old: @instance, db: @db).write
						else
							GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write)
						end
					end
				end
			end

			class GoToInvite
				def initialize(xmpp_uri)
					@xmpp_uri = xmpp_uri
				end

				def write
					Command.finish do |reply|
						oob = OOB.find_or_create(reply.command)
						oob.url = @xmpp_uri
					end
				end
			end
		end

		class CustomDomain < Snikket
			def initialize(customer, tel, old: nil, error: nil, db: DB)
				@customer = customer
				@tel = tel
				@error = error
				@old = old
				@db = db
			end

			def form
				FormTemplate.render(
					"registration/snikket_custom",
					tel: @tel,
					error: @error
				)
			end

			def write
				Command.reply { |reply|
					reply.allowed_actions = [:prev, :next]
					reply.command << form
				}.then do |iq|
					if iq.prev?
						Snikket.new(@customer, @tel, db: @db, old: @old).write
					else
						launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain)
					end
				end
			end
		end

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

			def write
				WelcomeMessage.new(@customer, @tel).welcome
				Command.finish("Your JMP account has been activated as #{@tel}")
			end
		end
	end
end
