# frozen_string_literal: true

require "value_semantics/monkey_patched"
require_relative "../admin_action"
require_relative "../form_to_h"
require_relative "../utils"

class AdminAction
	class NumberChange < AdminAction
		include Isomorphic
		class Command
			using FormToH

			def self.for(target_customer, reply:)
				EMPromise.resolve(
					new(
						customer_id: target_customer.customer_id,
						old_backend: target_customer.sgx&.strip!&.to_s,
						old_tel: (reg = target_customer.registered?) ? reg.phone : nil
					)
				).then { |x| reply.call(x.form).then(&x.method(:change)) }
			end

			def initialize(**bag)
				@bag = bag
			end

			def form
				FormTemplate.render("admin_number_change")
			end

			def change(result)
				AdminAction::NumberChange.for(
					**@bag,
					**result.form.to_h
						.reject { |_k, v| v == "nil" || v.to_s.empty? }
						.tap { |form_h|
							form_h["new_backend"] ||= @bag[:old_backend]
							form_h["new_tel"] ||= @bag[:old_tel]
						}
						.transform_keys(&:to_sym)
				)
			end
		end

		class Orphan < StandardError
			def to_s
				"Can't register nil tel to any backend"
			end
		end

		class UnknownBackend < StandardError
			def initialize(backend:, expected:)
				@backend = backend
				@expected = expected
			end

			def to_s
				"Got unknown backend: #{@backend}, expected one of #{@expected}"
			end
		end

		def customer_id
			@attributes[:customer_id]
		end

		def old_tel
			@attributes[:old_tel]
		end

		def new_tel
			@attributes[:new_tel]
		end

		def old_backend
			@attributes[:old_backend]
		end

		def new_backend
			@attributes[:new_backend]
		end

		def should_delete?
			["1", "true"].include?(@attributes[:should_delete])
		end

		def check_forward
			# Without this ordering, it's possible for
			# check_noop and check_orphan to race,
			# which creates ambiguity around whether
			# an actual error will raise or not.
			check_orphan.then {
				EMPromise.all([check_noop, check_backend])
			}
		end

		def forward
			EMPromise.all([
				old_tel && new_tel != old_tel && change_catapult_fwd!,
				update_tel_and_backend,
				should_disconnect_old_number? && disconnect_bandwidth_number
			]).then { self }
		end

		def update_tel_and_backend
			TrivialBackendSgxRepo.new(redis: REDIS).put(
				customer_id, new_backend
			).then { |sgx| sgx.register!(new_tel) }
		end

		# If old_tel exists, it's still possible no catapult_fwd has been set.
		# However, if one exists, we should rename it.
		# If old_tel does not exist, it's not possible for catapult_fwd
		# to exist, and it's not possible for there to be value.
		# to set catapult_fwd to. That's out-of-scope for this command.
		def change_catapult_fwd!
			REDIS.rename("catapult_fwd-#{old_tel}", "catapult_fwd-#{new_tel}")
		end

		def to_reverse
			with(
				old_tel: new_tel, new_tel: old_tel,
				old_backend: new_backend, new_backend: old_backend
			)
		end

		def to_s
			base = "Change Number\n"
			base += "	[move backend?]: #{old_backend} -> #{new_backend}\n"
			base += "	[change number?]: #{old_tel || 'nil'} -> #{new_tel}"
			base + delete_warning
		end

	protected

		def disconnect_bandwidth_number
			# Order name is limited to 40 characters
			# Assuming 12 chars for new_tel and 12 for customer_id, this is tight
			# but ok
			BandwidthTnRepo.new.disconnect(
				old_tel,
				"cust #{customer_id} swap to #{new_tel}"
			)
		end

		def delete_warning
			return "" unless should_delete? && !first_time?

			" * NOT DELETING"
		end

		def check_noop
			if new_tel == old_tel && new_backend == old_backend
				EMPromise.reject(NoOp.new)
			else
				EMPromise.resolve(nil)
			end
		end

		def check_orphan
			EMPromise.reject(Orphan.new) unless new_tel
		end

		def check_backend
			unless (expected = CONFIG[:sgx_creds].keys.map(
				&:to_s
			).push(CONFIG[:sgx])).include?(new_backend)
				EMPromise.reject(UnknownBackend.new(new_backend, expected))
			end
		end

		def should_disconnect_old_number?
			should_delete? &&
				first_time? &&
				old_tel != new_tel &&
				old_backend == CONFIG[:sgx]
		end
	end
end
