# frozen_string_literal: true

require "date"
require "em_promise"
require "lazy_object"
require "value_semantics/monkey_patched"

require_relative "blather_notify"

class Tel
	def initialize(str)
		@tel = if str.is_a? Tel
			str.to_s
		else
			"+1#{str.sub(/^\+?1?/, '')}"
		end
	end

	def to_s
		@tel
	end

	def ==(other)
		to_s == other.to_s
	end
end

class PortingStepRepo
	# Any thing that debounces messages must happen inside this.
	# The porting logic will pass everything in every time.
	class Outputs
		def info(port_id, kind, msg); end

		def warn(port_id, kind, msg); end

		def error(port_id, kind, e_or_msg); end

		def to_customer(port_id, kind, tel, msg); end
	end

	value_semantics do
		redis             Anything(), default: LazyObject.new { REDIS }
		blather_notify    Anything(), default: BlatherNotify
		admin_server      Anything(), default: CONFIG[:admin_server]
		testing_tel       Anything(), default: CONFIG[:testing_tel]
		output            Outputs, default: Outputs.new
	end

	def find(port)
		if_processing(port) do
			case port.processing_status
			when "FOC"
				FOC.new(**to_h).find(port)
			when "COMPLETE"
				Complete.new(**to_h).find(port)
			else
				Wait.new(port, output: output)
			end
		end
	end

	def if_processing(port)
		redis.exists("jmp_port_freeze-#{port.order_id}").then do |v|
			next Frozen.new(port, output: output) if v == 1

			yield
		end
	end

	class FOC < self
		# This is how long we'll wait for a port to move from FOC to COMPLETED
		# It's in fractional days because DateTime
		GRACE_PERIOD = 15.0 / (24 * 60)

		def find(port)
			Alert.for(
				port,
				grace_period: GRACE_PERIOD,
				output: output, key: :late_foc,
				msg: "⚠ Port is still in FOC state a while past FOC",
				real_step: Wait.new(port, output: output)
			)
		end
	end

	class Complete < self
		# If it's been 35 minutes and the number isn't reachable, a human
		# should get involved
		GRACE_PERIOD = 35.0 / (24 * 60)

		def find(port)
			if_not_complete(port) do
				exe = blather_notify.command_execution(admin_server, "customer info")
				AdminCommand.new(exe: exe, **to_h).find(port).then do |step|
					Alert.for(
						port,
						grace_period: GRACE_PERIOD, output: output,
						key: :late_finish, msg: msg(port), real_step: step
					)
				end
			end
		end

		def if_not_complete(port)
			redis.exists("jmp_port_complete-#{port.order_id}").then do |v|
				next Done.new(port, output: output) if v == 1

				yield
			end
		end

		def msg(port)
			"⚠ Port still hasn't finished. We'll keep trying unless you set redis "\
				"key `jmp_port_freeze-#{port.order_id}`"
		end

		class AdminCommand < self
			def initialize(exe:, **kwargs)
				@exe = exe
				super(**kwargs)
			end

			def to_h
				super.merge(exe: @exe)
			end

			def find(port)
				@exe.fetch_and_submit(q: port.customer_order_id).then do |form|
					tel = Tel.new(port.billing_telephone_number)
					if tel == Tel.new(form.tel)
						GoodNumber.new(**to_h).find(port)
					else
						WrongNumber.new(right_number: tel, execution: @exe)
					end
				end
			end

			class GoodNumber < self
				def find(port)
					Reachability.new(type: "voice", **to_h).test(port) do
						Reachability.new(type: "sms", **to_h).test(port) do
							FinishUp.new(port, redis: redis, output: output)
						end
					end
				end

				class Reachability < self
					def initialize(type:, **args)
						@type = type
						super(**args)
					end

					def test(port)
						execute_command(port).then do |response|
							if response.count == "0"
								RunTest.new(
									type: @type, tel: port.billing_telephone_number,
									**to_h.slice(:blather_notify, :testing_tel, :admin_server)
								)
							else
								yield
							end
						end
					end

					class RunTest
						# This is here for tests
						attr_reader :type

						def initialize(
							type:, tel:, blather_notify:, testing_tel:, admin_server:
						)
							@type = type
							@tel = tel
							@blather_notify = blather_notify
							@testing_tel = testing_tel
							@admin_server = admin_server
						end

						def perform_next_step
							@blather_notify.command_execution(@admin_server, "reachability")
								.fetch_and_submit(
									tel: @tel, type: @type, reachability_tel: @testing_tel
								)
						end
					end

				protected

					def execute_command(port)
						blather_notify.command_execution(admin_server, "reachability")
							.fetch_and_submit(tel: port.billing_telephone_number, type: @type)
					end
				end

				class FinishUp
					MESSAGE = "Hi!  This is JMP support - your number has "\
					"successfully transferred in to JMP!  All calls/messages "\
					"will now use your transferred-in number - your old JMP "\
					"number has been disabled.  Let us know if you have any "\
					"questions and thanks for using JMP!"

					def initialize(port, redis:, output:)
						@port = port
						@redis = redis
						@output = output
					end

					def set_key
						@redis.set(
							"jmp_port_complete-#{@port.order_id}",
							DateTime.now.iso8601,
							"EX",
							60 * 60 * 24 * 2 ### 2 Days should be enough to not see it listed
						)
					end

					def perform_next_step
						set_key.then do
							EMPromise.all([
								@output.info(@port.order_id, :complete, "Port Complete!"),
								@output.to_customer(
									@port.order_id, :complete,
									Tel.new(@port.billing_telephone_number), MESSAGE
								)
							])
						end
					end
				end
			end

			class WrongNumber
				def initialize(right_number:, execution:)
					@right_number = right_number
					@exe = execution
				end

				def perform_next_step
					@exe.fetch_and_submit(action: "number_change").then do |_form|
						@exe.fetch_and_submit(new_tel: @right_number, should_delete: "true")
					end
				end
			end
		end
	end

	# This doesn't do anything and just waits for something to happen later
	class Wait
		def initialize(port, output:)
			@port = port
			@output = output
		end

		def perform_next_step
			@output.info(@port.order_id, :wait, "Waiting...")
		end
	end

	# This also doesn't do anything but is more content about it
	class Done
		def initialize(port, output:)
			@port = port
			@output = output
		end

		def perform_next_step
			@output.info(@port.order_id, :done, "Done.")
		end
	end

	# This also also doesn't do anything but is intentional
	class Frozen
		def initialize(port, output:)
			@port = port
			@output = output
		end

		def perform_next_step
			@output.info(@port.order_id, :frozen, "Frozen.")
		end
	end

	# This class sends and error to the human to check things out
	class Alert
		def self.for(port, grace_period:, real_step:, **args)
			if (DateTime.now - port.actual_foc_date) > grace_period
				new(port, real_step: real_step, **args)
			else
				real_step
			end
		end

		# For tests
		attr_reader :key
		attr_reader :real_step

		def initialize(port, real_step:, output:, msg:, key:)
			@port = port
			@real_step = real_step
			@output = output
			@msg = msg
			@key = key
		end

		def perform_next_step
			@output.warn(@port.order_id, @key, @msg).then {
				@real_step.perform_next_step
			}
		end
	end
end
