package graphdb

import (
	"fmt"
	"time"

	"github.com/lightningnetwork/lnd/fn/v2"
	"github.com/lightningnetwork/lnd/lnwire"
)

const (
	// DefaultRejectCacheSize is the default number of rejectCacheEntries to
	// cache for use in the rejection cache of incoming gossip traffic. This
	// produces a cache size of around 1MB.
	DefaultRejectCacheSize = 50000

	// DefaultChannelCacheSize is the default number of ChannelEdges cached
	// in order to reply to gossip queries. This produces a cache size of
	// around 40MB.
	DefaultChannelCacheSize = 20000

	// DefaultPreAllocCacheNumNodes is the default number of channels we
	// assume for mainnet for pre-allocating the graph cache. As of
	// September 2021, there currently are 14k nodes in a strictly pruned
	// graph, so we choose a number that is slightly higher.
	DefaultPreAllocCacheNumNodes = 15000
)

// IteratorOption is a functional option used to change the per-call
// configuration for iterators.
type IteratorOption func(*iterConfig)

// iterConfig holds the configuration for graph operations.
type iterConfig struct {
	// chanUpdateIterBatchSize is the batch size to use when reading out
	// channel updates to send a peer a backlog.
	chanUpdateIterBatchSize int

	// nodeUpdateIterBatchSize is the batch size to use when reading out
	// node updates to send to a peer backlog.
	nodeUpdateIterBatchSize int

	// iterPublicNodes is used to make an iterator that only iterates over
	// public nodes.
	iterPublicNodes bool
}

// ChanUpdateRange describes a range for channel updates. Only one of the time
// or height ranges should be set depending on the gossip version.
type ChanUpdateRange struct {
	// StartTime is the inclusive lower time bound (v1 gossip only).
	StartTime fn.Option[time.Time]

	// EndTime is the exclusive upper time bound (v1 gossip only).
	EndTime fn.Option[time.Time]

	// StartHeight is the inclusive lower block-height bound (v2 gossip
	// only).
	StartHeight fn.Option[uint32]

	// EndHeight is the exclusive upper block-height bound (v2 gossip
	// only).
	EndHeight fn.Option[uint32]
}

// validateForVersion checks that the range fields are consistent with the
// given gossip version: v1 requires time bounds, v2 requires block-height
// bounds, and mixing the two is rejected.
func (r ChanUpdateRange) validateForVersion(v lnwire.GossipVersion) error {
	var (
		hasStartTime = r.StartTime.IsSome()
		hasEndTime   = r.EndTime.IsSome()
		hasTimeRange = hasStartTime || hasEndTime

		hasStartHeight = r.StartHeight.IsSome()
		hasEndHeight   = r.EndHeight.IsSome()
		hasBlockRange  = hasStartHeight || hasEndHeight
	)

	if hasTimeRange && hasBlockRange {
		return fmt.Errorf("chan update range has both time and block " +
			"ranges")
	}

	switch v {
	case gossipV1:
		if hasBlockRange {
			return fmt.Errorf("v1 chan update range must use time")
		}

		if !hasTimeRange {
			return fmt.Errorf("v1 chan update range missing time")
		}

		if !hasStartTime || !hasEndTime {
			return fmt.Errorf("v1 chan update range " +
				"missing time bounds")
		}

		start := r.StartTime.UnwrapOr(time.Time{})
		end := r.EndTime.UnwrapOr(time.Time{})

		if start.After(end) {
			return fmt.Errorf("v1 chan update range: " +
				"start time after end time")
		}

	case gossipV2:
		if hasTimeRange {
			return fmt.Errorf("v2 chan update range must use " +
				"blocks")
		}

		if !hasBlockRange {
			return fmt.Errorf("v2 chan update range missing " +
				"block range")
		}

		if !hasStartHeight || !hasEndHeight {
			return fmt.Errorf("v2 chan update range " +
				"missing block bounds")
		}

		start := r.StartHeight.UnwrapOr(0)
		end := r.EndHeight.UnwrapOr(0)
		if start > end {
			return fmt.Errorf("v2 chan update range: " +
				"start height after end height")
		}

	default:
		return fmt.Errorf("unknown gossip version: %v", v)
	}

	return nil
}

// NodeUpdateRange describes a range for node updates. Only one of the time or
// height ranges should be set depending on the gossip version.
type NodeUpdateRange struct {
	// StartTime is the inclusive lower time bound (v1 gossip only).
	StartTime fn.Option[time.Time]

	// EndTime is the exclusive upper time bound (v1 gossip only).
	EndTime fn.Option[time.Time]

	// StartHeight is the inclusive lower block-height bound (v2 gossip
	// only).
	StartHeight fn.Option[uint32]

	// EndHeight is the exclusive upper block-height bound (v2 gossip
	// only).
	EndHeight fn.Option[uint32]
}

// validateForVersion checks that the range fields are consistent with the
// given gossip version: v1 requires time bounds, v2 requires block-height
// bounds, and mixing the two is rejected.
func (r NodeUpdateRange) validateForVersion(v lnwire.GossipVersion) error {
	var (
		hasStartTime = r.StartTime.IsSome()
		hasEndTime   = r.EndTime.IsSome()

		hasStartHeight = r.StartHeight.IsSome()
		hasEndHeight   = r.EndHeight.IsSome()

		hasTimeRange  = hasStartTime || hasEndTime
		hasBlockRange = hasStartHeight || hasEndHeight
	)

	if hasTimeRange && hasBlockRange {
		return fmt.Errorf("node update range has both " +
			"time and block ranges")
	}

	switch v {
	case gossipV1:
		if hasBlockRange {
			return fmt.Errorf("v1 node update range must use time")
		}

		if !hasTimeRange {
			return fmt.Errorf("v1 node update range missing time")
		}
		if !hasStartTime || !hasEndTime {
			return fmt.Errorf("v1 node update range missing " +
				"time bounds")
		}

		start := r.StartTime.UnwrapOr(time.Time{})
		end := r.EndTime.UnwrapOr(time.Time{})
		if start.After(end) {
			return fmt.Errorf("v1 node update range: start time " +
				"after end time")
		}

	case gossipV2:
		if hasTimeRange {
			return fmt.Errorf("v2 node update range must use " +
				"height")
		}
		if !hasBlockRange {
			return fmt.Errorf("v2 node update range missing height")
		}
		if !hasStartHeight || !hasEndHeight {
			return fmt.Errorf("v2 node update range missing " +
				"height bounds")
		}

		start := r.StartHeight.UnwrapOr(0)
		end := r.EndHeight.UnwrapOr(0)
		if start > end {
			return fmt.Errorf("v2 node update range: start " +
				"height after end height")
		}

	default:
		return fmt.Errorf("unknown gossip version: %d", v)
	}

	return nil
}

// defaultIteratorConfig returns the default configuration.
func defaultIteratorConfig() *iterConfig {
	return &iterConfig{
		chanUpdateIterBatchSize: 1_000,
		nodeUpdateIterBatchSize: 1_000,
	}
}

// WithChanUpdateIterBatchSize sets the batch size for channel update
// iterators.
func WithChanUpdateIterBatchSize(size int) IteratorOption {
	return func(cfg *iterConfig) {
		if size > 0 {
			cfg.chanUpdateIterBatchSize = size
		}
	}
}

// WithNodeUpdateIterBatchSize set the batch size for node ann iterators.
func WithNodeUpdateIterBatchSize(size int) IteratorOption {
	return func(cfg *iterConfig) {
		if size > 0 {
			cfg.nodeUpdateIterBatchSize = size
		}
	}
}

// WithIterPublicNodesOnly is used to create an iterator that only iterates over
// public nodes.
func WithIterPublicNodesOnly() IteratorOption {
	return func(cfg *iterConfig) {
		cfg.iterPublicNodes = true
	}
}

// chanGraphOptions holds parameters for tuning and customizing the
// ChannelGraph.
type chanGraphOptions struct {
	// useGraphCache denotes whether the in-memory graph cache should be
	// used or a fallback version that uses the underlying database for
	// path finding.
	useGraphCache bool

	// preAllocCacheNumNodes is the number of nodes we expect to be in the
	// graph cache, so we can pre-allocate the map accordingly.
	preAllocCacheNumNodes int

	// asyncGraphCachePopulation indicates whether the graph cache
	// should be populated asynchronously or if the Start method should
	// block until the cache is fully populated. This is true by
	// default.
	asyncGraphCachePopulation bool
}

// defaultChanGraphOptions returns a new chanGraphOptions instance populated
// with default values.
func defaultChanGraphOptions() *chanGraphOptions {
	return &chanGraphOptions{
		useGraphCache:             true,
		asyncGraphCachePopulation: true,
		preAllocCacheNumNodes:     DefaultPreAllocCacheNumNodes,
	}
}

// ChanGraphOption describes the signature of a functional option that can be
// used to customize a ChannelGraph instance.
type ChanGraphOption func(*chanGraphOptions)

// WithUseGraphCache sets whether the in-memory graph cache should be used.
func WithUseGraphCache(use bool) ChanGraphOption {
	return func(o *chanGraphOptions) {
		o.useGraphCache = use
	}
}

// WithPreAllocCacheNumNodes sets the number of nodes we expect to be in the
// graph cache, so we can pre-allocate the map accordingly.
func WithPreAllocCacheNumNodes(n int) ChanGraphOption {
	return func(o *chanGraphOptions) {
		o.preAllocCacheNumNodes = n
	}
}

// WithAsyncGraphCachePopulation sets whether the graph cache should be
// populated asynchronously or if the Start method should block until the
// cache is fully populated.
func WithAsyncGraphCachePopulation(async bool) ChanGraphOption {
	return func(o *chanGraphOptions) {
		o.asyncGraphCachePopulation = async
	}
}

// WithSyncGraphCachePopulation will cause the ChannelGraph to block
// until the graph cache is fully populated before returning from the Start
// method. This is useful for tests that need to ensure the graph cache is
// fully populated before proceeding with further operations.
func WithSyncGraphCachePopulation() ChanGraphOption {
	return func(o *chanGraphOptions) {
		o.asyncGraphCachePopulation = false
	}
}

// StoreOptions holds parameters for tuning and customizing a graph DB.
type StoreOptions struct {
	// RejectCacheSize is the maximum number of rejectCacheEntries to hold
	// in the rejection cache.
	RejectCacheSize int

	// ChannelCacheSize is the maximum number of ChannelEdges to hold in the
	// channel cache.
	ChannelCacheSize int

	// BatchCommitInterval is the maximum duration the batch schedulers will
	// wait before attempting to commit a pending set of updates.
	BatchCommitInterval time.Duration

	// NoMigration specifies that underlying backend was opened in read-only
	// mode and migrations shouldn't be performed. This can be useful for
	// applications that use the channeldb package as a library.
	NoMigration bool
}

// DefaultOptions returns a StoreOptions populated with default values.
func DefaultOptions() *StoreOptions {
	return &StoreOptions{
		RejectCacheSize:  DefaultRejectCacheSize,
		ChannelCacheSize: DefaultChannelCacheSize,
		NoMigration:      false,
	}
}

// StoreOptionModifier is a function signature for modifying the default
// StoreOptions.
type StoreOptionModifier func(*StoreOptions)

// WithRejectCacheSize sets the RejectCacheSize to n.
func WithRejectCacheSize(n int) StoreOptionModifier {
	return func(o *StoreOptions) {
		o.RejectCacheSize = n
	}
}

// WithChannelCacheSize sets the ChannelCacheSize to n.
func WithChannelCacheSize(n int) StoreOptionModifier {
	return func(o *StoreOptions) {
		o.ChannelCacheSize = n
	}
}

// WithBatchCommitInterval sets the batch commit interval for the interval batch
// schedulers.
func WithBatchCommitInterval(interval time.Duration) StoreOptionModifier {
	return func(o *StoreOptions) {
		o.BatchCommitInterval = interval
	}
}
