# frozen_string_literal: true

require "lazy_object"
require "forwardable"

class PendingTransactionRepo
	class PendingTransaction
		extend Forwardable
		def_delegators :@tx, :tx_hash, :confirmations

		def initialize(tx, address)
			@tx = tx
			@address = address
		end

		def txid
			"#{tx_hash}/#{@address}"
		end

		def value
			@tx.amount_for(@address)
		end

		def outgoing?
			value <= 0
		end
	end

	def initialize(key, customer_address_template: nil, ignored_key: nil)
		@key = key
		@customer_address_template = customer_address_template
		@ignored_key = ignored_key

		# Default handler allows all exceptions to be rethrown
		@error_handler = ->(*) {}
	end

	def database
		@database ||= LazyObject.new { DB }
	end

	def redis
		@redis ||= LazyObject.new { REDIS }
	end

	def electrum
		@electrum ||= LazyObject.new { ELECTRUM }
	end

	def error_handler(&block)
		@error_handler = block
	end

	class ExistingTransactionFilter
		def initialize(database)
			@database = database
		end

		def prepare_sql(txids_and_customer_ids)
			values = txids_and_customer_ids.map { |(txid, _customer_id)|
				"('#{@database.escape_string(txid)}')"
			}

			<<-SQL
				SELECT
					transaction_id,
					transactions.transaction_id IS NOT NULL as exists
				FROM
					(VALUES #{values.join(',')}) AS t(transaction_id)
				LEFT JOIN transactions USING (transaction_id)
			SQL
		end

		def filter_chunk(txids_and_customer_ids)
			exists =
				@database
				.exec_params(prepare_sql(txids_and_customer_ids))
				.map { |row| [row["transaction_id"], row["exists"]] }
				.to_h

			txids_and_customer_ids.reject do |(txid, _customer_id)|
				exists[txid]
			end
		end
	end

	class IgnoredTransactionFilter
		def initialize(redis, key)
			@redis = redis
			@key = key
		end

		def filter_chunk(txids_and_customer_ids)
			# There's an smismember command that was added to the rubygem in
			# v4.5 but prod is currently v4.2.5 and I don't want to have to
			# deal with that.  So for now this sucks a little, using pipelining
			# instead.
			# Better than N full round-trip requests, but not as good as
			# smismember probably
			results = @redis.pipelined {
				txids_and_customer_ids.each do |(txid, _customer_id)|
					@redis.sismember(@key, txid)
				end
			}

			# Now filter by the results correspondingly
			# Because it's in order, we pull each true or false off results and
			# if it's true then we don't need to process the item further
			txids_and_customer_ids.reject { |_v| results.shift }
		end
	end

	class WrongCustomerFilter
		def initialize(redis, template)
			@redis = redis
			@template = template
		end

		def query_redis(txids_and_customer_ids)
			@redis.pipelined do
				txids_and_customer_ids.each do |(txid, customer_id)|
					_tx_hash, addr = txid.split("/")
					@redis.sismember(@template.call(customer_id), addr)
				end
			end
		end

		def filter_chunk(txids_and_customer_ids)
			results = query_redis(txids_and_customer_ids)

			txids_and_customer_ids.select do |(txid, customer_id)|
				r = results.shift
				warn "#{txid} doesn't match customer #{customer_id}" unless r
				r
			end
		end
	end

	def run_filters(txids_and_customer_ids)
		@filters ||= [
			IgnoredTransactionFilter.new(redis, @ignored_key),
			WrongCustomerFilter.new(redis, @customer_address_template),
			ExistingTransactionFilter.new(database)
		]

		@filters.reduce(txids_and_customer_ids) do |remaining_txs, f|
			next [] if remaining_txs.empty?

			f.filter_chunk(remaining_txs)
		end
	end

	# run_filters removes the ones we don't care about from the list, but that
	# list is just in memory. The rest of this removes it from the redis queue
	# so we're not just skipping over these things every time in the list but
	# otherwise leaving them there.
	def filter(txids_and_customer_ids)
		passed = run_filters(txids_and_customer_ids)

		unless txids_and_customer_ids.length == passed.length
			redis.hdel(
				@key,
				(txids_and_customer_ids - passed).map(&:first)
			)
		end

		passed
	end

	def build_transaction(txid)
		tx_hash, address = txid.split("/", 2)
		txn = electrum.gettransaction(tx_hash)

		PendingTransaction.new(txn, address)
	end

	def map(chunk_size: 200)
		redis.hgetall(@key).each_slice(chunk_size).map { |chunk|
			filter(chunk).map { |(txid, customer_id)|
				begin
					yield [build_transaction(txid), customer_id]
				rescue StandardError => e
					raise e unless @error_handler.call(e)
				end
			}.compact
		}.flatten
	end

	def remove_transaction(pending)
		redis.hdel(@key, pending.txid)
	end

	# We use this to record transactions we've decided not to care about ever
	# again. These get filtered out during the processing _before_ it gets to
	# electrum or the user code, so effectively they just don't exist anymore.
	#
	# If electrum was more efficient this would be unnecessary because we'd
	# just ask electrum about the thing and we could decide we don't care
	# organically. But since it's a bit sensitive, this effectively serves as a
	# cache of "things we'd end up ignoring anyway"
	def mark_ignored(pending)
		redis.sadd(@ignored_key, pending.txid)
	end
end
