package libp2p

import (
	"context"

	"github.com/ipfs/kubo/config"
	"github.com/libp2p/go-libp2p"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/libp2p/go-libp2p/p2p/host/autorelay"
	"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
	"go.uber.org/fx"
)

func RelayTransport(enableRelay bool) func() (opts Libp2pOpts, err error) {
	return func() (opts Libp2pOpts, err error) {
		if enableRelay {
			opts.Opts = append(opts.Opts, libp2p.EnableRelay())
		} else {
			opts.Opts = append(opts.Opts, libp2p.DisableRelay())
		}
		return
	}
}

func RelayService(enable bool, relayOpts config.RelayService) func() (opts Libp2pOpts, err error) {
	return func() (opts Libp2pOpts, err error) {
		if enable {
			def := relay.DefaultResources()
			// Real defaults live in go-libp2p.
			// Here we apply any overrides from user config.
			opts.Opts = append(opts.Opts, libp2p.EnableRelayService(relay.WithResources(relay.Resources{
				Limit: &relay.RelayLimit{
					Data:     relayOpts.ConnectionDataLimit.WithDefault(def.Limit.Data),
					Duration: relayOpts.ConnectionDurationLimit.WithDefault(def.Limit.Duration),
				},
				MaxCircuits:            int(relayOpts.MaxCircuits.WithDefault(int64(def.MaxCircuits))),
				BufferSize:             int(relayOpts.BufferSize.WithDefault(int64(def.BufferSize))),
				ReservationTTL:         relayOpts.ReservationTTL.WithDefault(def.ReservationTTL),
				MaxReservations:        int(relayOpts.MaxReservations.WithDefault(int64(def.MaxReservations))),
				MaxReservationsPerIP:   int(relayOpts.MaxReservationsPerIP.WithDefault(int64(def.MaxReservationsPerIP))),
				MaxReservationsPerPeer: int(relayOpts.MaxReservationsPerPeer.WithDefault(int64(def.MaxReservationsPerPeer))),
				MaxReservationsPerASN:  int(relayOpts.MaxReservationsPerASN.WithDefault(int64(def.MaxReservationsPerASN))),
			})))
		}
		return
	}
}

func MaybeAutoRelay(staticRelays []string, cfgPeering config.Peering, enabled bool) fx.Option {
	if !enabled {
		return fx.Options()
	}

	if len(staticRelays) > 0 {
		return fx.Provide(func() (opts Libp2pOpts, err error) {
			if len(staticRelays) > 0 {
				static := make([]peer.AddrInfo, 0, len(staticRelays))
				for _, s := range staticRelays {
					var addr *peer.AddrInfo
					addr, err = peer.AddrInfoFromString(s)
					if err != nil {
						return
					}
					static = append(static, *addr)
				}
				opts.Opts = append(opts.Opts, libp2p.EnableAutoRelayWithStaticRelays(static))
			}
			return
		})
	}

	peerChan := make(chan peer.AddrInfo)
	return fx.Options(
		// Provide AutoRelay option
		fx.Provide(func() (opts Libp2pOpts, err error) {
			opts.Opts = append(opts.Opts,
				libp2p.EnableAutoRelayWithPeerSource(
					func(ctx context.Context, numPeers int) <-chan peer.AddrInfo {
						// TODO(9257): make this code smarter (have a state and actually try to grow the search outward) instead of a long running task just polling our K cluster.
						r := make(chan peer.AddrInfo)
						go func() {
							defer close(r)
							for ; numPeers != 0; numPeers-- {
								select {
								case v, ok := <-peerChan:
									if !ok {
										return
									}
									select {
									case r <- v:
									case <-ctx.Done():
										return
									}
								case <-ctx.Done():
									return
								}
							}
						}()
						return r
					},
					autorelay.WithMinInterval(0),
				))
			return
		}),
		autoRelayFeeder(cfgPeering, peerChan),
	)
}

func HolePunching(flag config.Flag, hasRelayClient bool) func() (opts Libp2pOpts, err error) {
	return func() (opts Libp2pOpts, err error) {
		if flag.WithDefault(true) {
			if !hasRelayClient {
				// If hole punching is explicitly enabled but the relay client is disabled then panic,
				// otherwise just silently disable hole punching
				if flag != config.Default {
					log.Fatal("Failed to enable `Swarm.EnableHolePunching`, it requires `Swarm.RelayClient.Enabled` to be true.")
				} else {
					log.Info("HolePunching has been disabled due to the RelayClient being disabled.")
				}
				return
			}
			opts.Opts = append(opts.Opts, libp2p.EnableHolePunching())
		}
		return
	}
}
