# frozen_string_literal: true

require "multibases"
require "securerandom"

class InvitesRepo
	class Invalid < StandardError; end

	def initialize(db=DB, redis=REDIS)
		@db = db
		@redis = redis
	end

	def unused_invites(customer_id)
		promise = @db.query_defer(<<~SQL, [customer_id])
			SELECT code FROM unused_invites WHERE creator_id=$1
		SQL
		promise.then { |result| result.map { |row| row["code"] } }
	end

	def find_or_create_group_code(customer_id)
		@redis.get("jmp_customer_group_code-#{customer_id}").then do |code|
			next code if code

			code = Multibases.pack("base32upper", SecureRandom.bytes(4)).to_s
			EMPromise.all([
				@redis.set("jmp_customer_group_code-#{customer_id}", code),
				@redis.hset("jmp_group_codes", code, customer_id)
			]).then { code }
		end
	end

	def stash_code(customer_id, code)
		return EMPromise.resolve(nil) if code.to_s.strip == ""

		@redis.set("jmp_customer_pending_invite-#{customer_id}", code)
	end

	def use_pending_group_code(customer_id)
		@redis.get("jmp_customer_pending_invite-#{customer_id}").then do |code|
			EMPromise.all([
				@redis.del("jmp_customer_pending_invite-#{customer_id}"),
				@redis.hget("jmp_group_codes", code)
			]).then do |(_, credit_to)|
				next false if credit_to.to_s.strip == ""

				create_claimed_code(credit_to, customer_id)
				credit_to
			end
		end
	end

	CLAIM_SQL = <<~SQL
		UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP
		WHERE code=$2 AND used_by_id IS NULL
	SQL

	def claim_code(customer_id, code, &blk)
		raise Invalid, "No code provided" if code.to_s.strip == ""

		guard_too_many_tries(customer_id).then do
			@db.transaction do
				valid = @db.exec(CLAIM_SQL, [customer_id, code]).cmd_tuples.positive?
				invalid_code(customer_id, code).sync unless valid

				blk.call
			end
		end
	end

	def create_claimed_code(creator_id, used_by_id)
		@db.exec(<<~SQL, [creator_id, used_by_id])
			INSERT INTO invites (creator_id, used_by_id, used_at)
			VALUES ($1, $2, LOCALTIMESTAMP)
		SQL
	end

	CREATE_N_SQL = <<~SQL
		INSERT INTO invites
			SELECT unnest(array_fill($1::text, array[$2::int]))
		RETURNING code
	SQL

	def create_n_codes(customer_id, num)
		EMPromise.resolve(nil).then {
			codes = @db.exec(CREATE_N_SQL, [customer_id, num])
			raise Invalid, "Failed to fetch codes" unless codes.cmd_tuples.positive?

			codes.map { |row| row["code"] }
		}
	end

	def any_existing?(codes)
		promise = @db.query_one(<<~SQL, [codes])
			SELECT count(1) FROM invites WHERE code = ANY($1)
		SQL
		promise.then { |result| result[:count].positive? }
	end

	def any_claimed?(codes)
		promise = @db.query_one(<<~SQL, [codes])
			SELECT count(1) FROM invites WHERE code = ANY($1) AND used_by_id IS NOT NULL
		SQL
		promise.then { |result| result[:count].positive? }
	end

	def create_codes(customer_id, codes)
		custs = [customer_id] * codes.length
		EMPromise.resolve(nil).then {
			@db.transaction do
				valid = @db.exec(<<~SQL, [custs, codes]).cmd_tuples.positive?
					INSERT INTO invites(creator_id, code) SELECT unnest($1), unnest($2)
				SQL
				raise Invalid, "Failed to insert one of: #{codes}" unless valid
			end
		}
	end

	def delete_codes(codes)
		EMPromise.resolve(nil).then {
			@db.exec(<<~SQL, [codes])
				DELETE FROM invites WHERE code = ANY($1)
			SQL
		}
	end

protected

	def guard_too_many_tries(customer_id)
		@redis.get("jmp_invite_tries-#{customer_id}").then do |t|
			raise Invalid, "Too many wrong attempts" if t.to_i > 10
		end
	end

	def invalid_code(customer_id, code)
		@redis.incr("jmp_invite_tries-#{customer_id}").then {
			@redis.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
		}.then {
			@redis.hexists("jmp_group_codes", code)
		}.then { |is_group|
			raise Invalid, "#{code} is a post-payment referral" if is_group.to_i == 1

			raise Invalid, "Not a valid invite code: #{code}"
		}
	end
end
