# frozen_string_literal: true

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

require_relative "blather_notify"
require_relative "utils"

class PortingStepRepo
	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

	# 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.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.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.id}`"
		end

		class NoCustomer < self
			attr_reader :port

			NO_GRACE_PERIOD = 0

			def initialize(port:, **kwargs)
				@port = port
				super(**kwargs)
			end

			def self.for(port:, **kwargs)
				Alert.for(
					port,
					grace_period: NO_GRACE_PERIOD,
					real_step: new(port: port, **kwargs),
					output: kwargs[:output],
					msg: msg(port),
					key: :port_for_unknown_customer
				)
			end

			def self.msg(port)
				"⚠ Freezing port #{port.id} for unknown customer: #{port.customer_id}."
			end

			def perform_next_step
				redis.set("jmp_port_freeze-#{port.id}", 1)
			end
		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_id).then do |form|
					next NoCustomer.for(port: port, **to_h.except(:exe)) unless form.tel

					tel = Tel.new(port.tel)
					good = GoodNumber.new(**to_h).find(port)
					wrong = WrongNumber.new(
						right_number: tel, execution: @exe, new_backend: port.backend_sgx
					)
					tel == Tel.new(form.tel) ? good : wrong
				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
				rescue StandardError => e
					ReachabilityFailure.for(error: e, port: port, **to_h)
				end

				class ReachabilityFailure < self
					attr_reader :error

					NO_GRACE_PERIOD = 0

					# @param error [StandardError]
					def self.for(port:, error:, **kwargs)
						Alert.for(
							port,
							grace_period: NO_GRACE_PERIOD,
							real_step: nil,
							output: kwargs[:output],
							msg: msg(port, error),
							key: :reachability_failure
						)
					end

					def self.msg(port, error)
						"⚠ Error checking #{port.id} reachability: #{error}"
					end
				end

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

					def test(port)
						execute_command(port).then { |response|
							next yield unless response.count == "0"

							args = to_h.slice(
								:blather_notify, :testing_tel, :admin_server, :output
							)
							RunTest.new(type: @type, tel: port.tel, port: port, **args)
						}.catch do |e|
							ReachabilityFailure.for(port: port, error: e, output: output)
						end
					end

					class RunTest
						# This is here for tests
						attr_reader :type

						def initialize(
							type:, tel:, port:, output:,
							blather_notify:, testing_tel:, admin_server:
						)
							@type = type
							@tel = tel
							@port = port
							@output = output
							@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
								).catch do |e|
									ReachabilityFailure.for(
										port: @port, error: e, output: @output
									).perform_next_step
								end
						end
					end

				protected

					def execute_command(port)
						blather_notify.command_execution(admin_server, "reachability")
							.fetch_and_submit(tel: port.tel, 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.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.id, :complete, "Port Complete!"),
								@output.to_customer(
									@port.id, :complete,
									Tel.new(@port.tel), MESSAGE
								)
							])
						end
					end
				end
			end

			class WrongNumber
				attr_reader :new_backend

				def initialize(
					right_number:,
					execution:,
					new_backend:
				)
					@new_backend = new_backend
					@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_backend: @new_backend,
							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.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.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.id, :frozen, "Frozen.")
		end
	end

	# This class sends and error to the human to check things out
	class Alert
		# @param [PortingStepRepo, NilClass] real_step If `nil`, just notify
		def self.for(port, grace_period:, real_step:, **args)
			if (DateTime.now - port.actual_foc_date.to_datetime) > 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.id, @key, @msg).then {
				@real_step&.perform_next_step
			}
		end
	end
end

class FullManual < PortingStepRepo::Outputs
	def info(id, key, msg)
		puts "[#{id}] INFO(#{key}): #{msg}"
	end

	def warn(id, key, msg)
		puts "[#{id}] WARN(#{key}): #{msg}"
	end

	def error(id, key, e_or_msg)
		puts "[#{id}] ERRR(#{key}): #{e_or_msg}"
		return unless e_or_msg.respond_to?(:backtrace)

		e_or_msg.backtrace.each do |b|
			puts "[#{id}] ERRR(#{key}): #{b}"
		end
	end

	def to_customer(id, key, tel, msg)
		puts "[#{id}] CUST(#{key}, #{tel}): #{msg}"
	end
end

class ObservedAuto < FullManual
	def initialize(endpoint, source_number)
		@endpoint = endpoint
		@src = source_number
	end

	def to_customer(id, key, tel, msg)
		ExpiringLock.new(lock_key(id, key)).with do
			EM::HttpRequest
				.new(@endpoint)
				.apost(
					head: { "Content-Type" => "application/json" },
					body: format_msg(tel, msg)
				)
		end
	end

protected

	def lock_key(id, key)
		"jmp_port_customer_msg_#{key}-#{id}"
	end

	def format_msg(tel, msg)
		[{
			time: DateTime.now.iso8601,
			type: "message-received",
			to: tel,
			description: "Incoming message received",
			message: actual_message(tel, msg)
		}].to_json
	end

	def actual_message(tel, msg)
		{
			id: SecureRandom.uuid,
			owner: tel,
			applicationId: SecureRandom.uuid,
			time: DateTime.now.iso8601,
			segmentCount: 1,
			direction: "in",
			to: [tel], from: @src,
			text: msg
		}
	end
end

class FullAutomatic < ObservedAuto
	using FormToH

	def initialize(pubsub_addr, endpoint, source_number)
		@pubsub = BlatherNotify.pubsub(pubsub_addr)

		Sentry.init do |config|
			config.background_worker_threads = 0
		end

		super(endpoint, source_number)
	end

	# No one's watch; swallow informational messages
	def info(*); end

	def warn(id, key, msg)
		ExpiringLock.new(warn_lock_key(id, key), expiry: 60 * 15).with do
			entrykey = "#{id}:#{key}"
			@pubsub.publish("#{entrykey}": error_entry("Port Warning", msg, entrykey))
		end
	end

	def error(id, key, e_or_msg)
		Sentry.with_scope do |scope|
			scope.set_context("port", { id: id, action: key })

			if e_or_msg.is_a?(::Exception)
				Sentry.capture_exception(e_or_msg)
			else
				Sentry.capture_message(e_or_msg.to_s)
			end
		end
	end

protected

	def error_entry(title, text, id)
		Nokogiri::XML::Builder.new { |xml|
			xml.entry(xmlns: "http://www.w3.org/2005/Atom") do
				xml.updated DateTime.now.iso8601
				xml.id id
				xml.title title
				xml.content text.to_s, type: "text"
				xml.author { xml.name "porting" }
				xml.generator "porting", version: "1.0"
			end
		}.doc.root
	end

	def warn_lock_key(id, key)
		"jmp_port_warn_msg_#{key}-#{id}"
	end
end
