package node

import (
	"context"
	"fmt"
	"time"

	"github.com/ipfs/boxo/fetcher"
	pin "github.com/ipfs/boxo/pinning/pinner"
	provider "github.com/ipfs/boxo/provider"
	"github.com/ipfs/boxo/provider/batched"
	q "github.com/ipfs/boxo/provider/queue"
	"github.com/ipfs/boxo/provider/simple"
	"go.uber.org/fx"

	"github.com/ipfs/kubo/core/node/helpers"
	"github.com/ipfs/kubo/repo"
	irouting "github.com/ipfs/kubo/routing"
)

// SIMPLE

// ProviderQueue creates new datastore backed provider queue
func ProviderQueue(mctx helpers.MetricsCtx, lc fx.Lifecycle, repo repo.Repo) (*q.Queue, error) {
	return q.NewQueue(helpers.LifecycleCtx(mctx, lc), "provider-v1", repo.Datastore())
}

// SimpleProvider creates new record provider
func SimpleProvider(mctx helpers.MetricsCtx, lc fx.Lifecycle, queue *q.Queue, rt irouting.ProvideManyRouter) provider.Provider {
	return simple.NewProvider(helpers.LifecycleCtx(mctx, lc), queue, rt)
}

// SimpleReprovider creates new reprovider
func SimpleReprovider(reproviderInterval time.Duration) interface{} {
	return func(mctx helpers.MetricsCtx, lc fx.Lifecycle, rt irouting.ProvideManyRouter, keyProvider simple.KeyChanFunc) (provider.Reprovider, error) {
		return simple.NewReprovider(helpers.LifecycleCtx(mctx, lc), reproviderInterval, rt, keyProvider), nil
	}
}

// SimpleProviderSys creates new provider system
func SimpleProviderSys(isOnline bool) interface{} {
	return func(lc fx.Lifecycle, p provider.Provider, r provider.Reprovider) provider.System {
		sys := provider.NewSystem(p, r)

		if isOnline {
			lc.Append(fx.Hook{
				OnStart: func(ctx context.Context) error {
					sys.Run()
					return nil
				},
				OnStop: func(ctx context.Context) error {
					return sys.Close()
				},
			})
		}

		return sys
	}
}

// BatchedProviderSys creates new provider system
func BatchedProviderSys(isOnline bool, reprovideInterval time.Duration) interface{} {
	return func(lc fx.Lifecycle, cr irouting.ProvideManyRouter, q *q.Queue, keyProvider simple.KeyChanFunc, repo repo.Repo) (provider.System, error) {
		sys, err := batched.New(cr, q,
			batched.ReproviderInterval(reprovideInterval),
			batched.Datastore(repo.Datastore()),
			batched.KeyProvider(keyProvider))
		if err != nil {
			return nil, err
		}

		if isOnline {
			lc.Append(fx.Hook{
				OnStart: func(ctx context.Context) error {
					sys.Run()
					return nil
				},
				OnStop: func(ctx context.Context) error {
					return sys.Close()
				},
			})
		}

		return sys, nil
	}
}

// ONLINE/OFFLINE

// OnlineProviders groups units managing provider routing records online
func OnlineProviders(useStrategicProviding bool, useBatchedProviding bool, reprovideStrategy string, reprovideInterval time.Duration) fx.Option {
	if useStrategicProviding {
		return fx.Provide(provider.NewOfflineProvider)
	}

	return fx.Options(
		SimpleProviders(reprovideStrategy, reprovideInterval),
		maybeProvide(SimpleProviderSys(true), !useBatchedProviding),
		maybeProvide(BatchedProviderSys(true, reprovideInterval), useBatchedProviding),
	)
}

// OfflineProviders groups units managing provider routing records offline
func OfflineProviders(useStrategicProviding bool, useBatchedProviding bool, reprovideStrategy string, reprovideInterval time.Duration) fx.Option {
	if useStrategicProviding {
		return fx.Provide(provider.NewOfflineProvider)
	}

	return fx.Options(
		SimpleProviders(reprovideStrategy, reprovideInterval),
		maybeProvide(SimpleProviderSys(false), true),
		//maybeProvide(BatchedProviderSys(false, reprovideInterval), useBatchedProviding),
	)
}

// SimpleProviders creates the simple provider/reprovider dependencies
func SimpleProviders(reprovideStrategy string, reproviderInterval time.Duration) fx.Option {
	var keyProvider fx.Option
	switch reprovideStrategy {
	case "all":
		fallthrough
	case "":
		keyProvider = fx.Provide(simple.NewBlockstoreProvider)
	case "roots":
		keyProvider = fx.Provide(pinnedProviderStrategy(true))
	case "pinned":
		keyProvider = fx.Provide(pinnedProviderStrategy(false))
	default:
		return fx.Error(fmt.Errorf("unknown reprovider strategy '%s'", reprovideStrategy))
	}

	return fx.Options(
		fx.Provide(ProviderQueue),
		fx.Provide(SimpleProvider),
		keyProvider,
		fx.Provide(SimpleReprovider(reproviderInterval)),
	)
}

func pinnedProviderStrategy(onlyRoots bool) interface{} {
	type input struct {
		fx.In
		Pinner      pin.Pinner
		IPLDFetcher fetcher.Factory `name:"ipldFetcher"`
	}
	return func(in input) simple.KeyChanFunc {
		return simple.NewPinnedProvider(onlyRoots, in.Pinner, in.IPLDFetcher)
	}
}
