# frozen_string_literal: true

require "lazy_object"
require "value_semantics/monkey_patched"

require_relative "bandwidth_tn_repo"
require_relative "customer"
require_relative "polyfill"

class CustomerRepo
	class NotFound < RuntimeError; end

	value_semantics do
		set_user          Proc,       default: ->(**) {}, coerce: :to_proc.to_proc
		redis             Anything(), default: LazyObject.new { REDIS }
		db                Anything(), default: LazyObject.new { DB }
		braintree         Anything(), default: LazyObject.new { BRAINTREE }
		sgx_repo          Anything(), default: TrivialBackendSgxRepo.new
		bandwidth_tn_repo Anything(), default: BandwidthTnRepo.new
	end

	module QueryKey
		def self.for(s)
			case s
			when Blather::JID, ProxiedJID
				JID.for(s)
			when /\Axmpp:(.*)/
				JID.for($1)
			when /\A(?:tel:)?(\+\d+)\Z/
				Tel.new($1)
			else
				ID.new(s)
			end
		end

		ID = Struct.new(:customer_id) do
			def keys(redis)
				redis.get("jmp_customer_jid-#{customer_id}").then do |jid|
					raise NotFound, "No jid" unless jid

					[customer_id, jid]
				end
			end
		end

		JID = Struct.new(:jid) do
			def self.for(jid)
				if jid.to_s =~ /\Acustomer_(.+)@#{CONFIG[:component][:jid]}\Z/
					ID.new($1)
				else
					new(jid)
				end
			end

			def keys(redis)
				redis.get("jmp_customer_id-#{jid}").then do |customer_id|
					raise NotFound, "No customer" unless customer_id

					[customer_id, jid]
				end
			end
		end

		Tel = Struct.new(:tel) do
			def keys(redis)
				redis.get("catapult_jid-#{tel}").then do |jid|
					raise NotFound, "No jid" unless jid

					JID.for(jid).keys(redis)
				end
			end
		end
	end

	def find(customer_id)
		set_user.call(customer_id: customer_id)
		QueryKey::ID.new(customer_id).keys(redis).then { |k| find_inner(*k) }
	end

	def find_by_jid(jid)
		set_user.call(jid: jid)
		QueryKey::JID.for(jid).keys(redis).then { |k| find_inner(*k) }
	end

	def find_by_tel(tel)
		set_user.call(tel: tel)
		QueryKey::Tel.new(tel).keys(redis).then { |k| find_inner(*k) }
	end

	def find_by_format(s)
		set_user.call(q: s)
		QueryKey.for(s).keys(redis).then { |k| find_inner(*k) }
	end

	def create(jid)
		@braintree.customer.create.then do |result|
			raise "Braintree customer create failed" unless result.success?

			cid = result.customer.id
			@redis.msetnx(
				"jmp_customer_id-#{jid}", cid, "jmp_customer_jid-#{cid}", jid
			).then do |redis_result|
				raise "Saving new customer to redis failed" unless redis_result == 1

				Customer.new(cid, Blather::JID.new(jid), sgx: new_sgx(cid))
			end
		end
	end

	def put_lidb_name(customer, lidb_name)
		@bandwidth_tn_repo.put_lidb_name(customer.registered?.phone, lidb_name)
	end

	def put_transcription_enabled(customer, enabled)
		@sgx_repo.put_transcription_enabled(customer.customer_id, enabled)
	end

	def put_fwd(customer, customer_fwd)
		tel = customer.registered?.phone
		@sgx_repo.put_fwd(customer.customer_id, tel, customer_fwd)
	end

	def put_monthly_overage_limit(customer, limit)
		k = "jmp_customer_monthly_overage_limit-#{customer.customer_id}"
		@redis.set(k, limit)
	end

	def change_jid(customer, new_jid)
		@redis.set("jmp_customer_id-#{new_jid}", customer.customer_id).then {
			@redis.set("jmp_customer_jid-#{customer.customer_id}", new_jid)
		}.then {
			SwapDefaultFwd.new.do(self, customer, new_jid)
		}.then do
			@redis.del("jmp_customer_id-#{customer.jid}")
		end
	end

	# I've put this here to hide the lines from rubocop
	# After we sort out where call routing should live, this whole process will
	# no longer be necessary
	class SwapDefaultFwd
		def do(repo, customer, new_jid)
			unless customer.fwd.uri == "xmpp:#{customer.jid}"
				return EMPromise.resolve(nil)
			end

			repo.put_fwd(customer, customer.fwd.with(uri: "xmpp:#{new_jid}"))
		end
	end

protected

	def new_sgx(customer_id)
		TrivialBackendSgxRepo.new.get(customer_id).with(registered?: false)
	end

	def mget(*keys)
		@redis.mget(*keys).then { |values| Hash[keys.zip(values.map(&:to_i))] }
	end

	def fetch_redis(customer_id)
		mget(
			"jmp_customer_auto_top_up_amount-#{customer_id}",
			"jmp_customer_monthly_overage_limit-#{customer_id}"
		).then { |r|
			r.transform_keys { |k| k.match(/^jmp_customer_([^-]+)/)[1].to_sym }
		}
	end

	SQL = <<~SQL
		SELECT COALESCE(balance,0) AS balance, plan_name, expires_at
		FROM customer_plans LEFT JOIN balances USING (customer_id)
		WHERE customer_id=$1 LIMIT 1
	SQL

	def fetch_all(customer_id)
		EMPromise.all([
			@sgx_repo.get(customer_id).then { |sgx| { sgx: sgx } },
			@db.query_one(SQL, customer_id, default: {}),
			fetch_redis(customer_id)
		]).then { |all| all.reduce(&:merge) }
	end

	def tndetails(sgx)
		return {} unless sgx.registered?

		LazyObject.new { @bandwidth_tn_repo.find(sgx.registered?.phone) || {} }
	end

	def find_inner(customer_id, jid)
		set_user.call(customer_id: customer_id, jid: jid)
		fetch_all(customer_id).then do |data|
			Customer.new(
				customer_id, Blather::JID.new(jid),
				tndetails: tndetails(data[:sgx]),
				plan: CustomerPlan.extract(customer_id, data),
				**data.slice(:balance, :sgx, :tndetails)
			)
		end
	end
end
