package dependency

import (
	"context"
	"errors"
	iofs "io/fs"
	"net/url"
	"sync"
	"time"

	"github.com/cloudreve/Cloudreve/v4/application/statics"
	"github.com/cloudreve/Cloudreve/v4/ent"
	"github.com/cloudreve/Cloudreve/v4/inventory"
	"github.com/cloudreve/Cloudreve/v4/pkg/auth"
	"github.com/cloudreve/Cloudreve/v4/pkg/cache"
	"github.com/cloudreve/Cloudreve/v4/pkg/cluster"
	"github.com/cloudreve/Cloudreve/v4/pkg/conf"
	"github.com/cloudreve/Cloudreve/v4/pkg/credmanager"
	"github.com/cloudreve/Cloudreve/v4/pkg/email"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/encrypt"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/eventhub"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/mime"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock"
	"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
	"github.com/cloudreve/Cloudreve/v4/pkg/logging"
	"github.com/cloudreve/Cloudreve/v4/pkg/mediameta"
	"github.com/cloudreve/Cloudreve/v4/pkg/queue"
	"github.com/cloudreve/Cloudreve/v4/pkg/request"
	"github.com/cloudreve/Cloudreve/v4/pkg/setting"
	"github.com/cloudreve/Cloudreve/v4/pkg/thumb"
	"github.com/cloudreve/Cloudreve/v4/pkg/util"
	"github.com/gin-contrib/static"
	"github.com/go-webauthn/webauthn/webauthn"
	"github.com/robfig/cron/v3"
	"github.com/samber/lo"
	"github.com/ua-parser/uap-go/uaparser"
)

var (
	ErrorConfigPathNotSet = errors.New("config path not set")
)

type (
	// DepCtx defines keys for dependency manager
	DepCtx struct{}
	// ReloadCtx force reload new dependency
	ReloadCtx struct{}
)

// Dep manages all dependencies of the server application. The default implementation is not
// concurrent safe, so all inner deps should be initialized before any goroutine starts.
type Dep interface {
	// ConfigProvider Get a singleton conf.ConfigProvider instance.
	ConfigProvider() conf.ConfigProvider
	// Logger Get a singleton logging.Logger instance.
	Logger() logging.Logger
	// Statics Get a singleton fs.FS instance for embedded static resources.
	Statics() iofs.FS
	// ServerStaticFS Get a singleton static.ServeFileSystem instance for serving static resources.
	ServerStaticFS() static.ServeFileSystem
	// DBClient Get a singleton ent.Client instance for database access.
	DBClient() *ent.Client
	// KV Get a singleton cache.Driver instance for KV store.
	KV() cache.Driver
	// NavigatorStateKV Get a singleton cache.Driver instance for navigator state store. It forces use in-memory
	// map instead of Redis to get better performance for complex nested linked list.
	NavigatorStateKV() cache.Driver
	// SettingClient Get a singleton inventory.SettingClient instance for access DB setting store.
	SettingClient() inventory.SettingClient
	// SettingProvider Get a singleton setting.Provider instance for access setting store in strong type.
	SettingProvider() setting.Provider
	// UserClient Creates a new inventory.UserClient instance for access DB user store.
	UserClient() inventory.UserClient
	// GroupClient Creates a new inventory.GroupClient instance for access DB group store.
	GroupClient() inventory.GroupClient
	// EmailClient Get a singleton email.Driver instance for sending emails.
	EmailClient(ctx context.Context) email.Driver
	// GeneralAuth Get a singleton auth.Auth instance for general authentication.
	GeneralAuth() auth.Auth
	// Shutdown the dependencies gracefully.
	Shutdown(ctx context.Context) error
	// FileClient Creates a new inventory.FileClient instance for access DB file store.
	FileClient() inventory.FileClient
	// NodeClient Creates a new inventory.NodeClient instance for access DB node store.
	NodeClient() inventory.NodeClient
	// DavAccountClient Creates a new inventory.DavAccountClient instance for access DB dav account store.
	DavAccountClient() inventory.DavAccountClient
	// DirectLinkClient Creates a new inventory.DirectLinkClient instance for access DB direct link store.
	DirectLinkClient() inventory.DirectLinkClient
	// OAuthClientClient Creates a new inventory.OAuthClientClient instance for access DB OAuth client store.
	OAuthClientClient() inventory.OAuthClientClient
	// HashIDEncoder Get a singleton hashid.Encoder instance for encoding/decoding hashids.
	HashIDEncoder() hashid.Encoder
	// TokenAuth Get a singleton auth.TokenAuth instance for token authentication.
	TokenAuth() auth.TokenAuth
	// LockSystem Get a singleton lock.LockSystem instance for file lock management.
	LockSystem() lock.LockSystem
	// ShareClient Creates a new inventory.ShareClient instance for access DB share store.
	StoragePolicyClient() inventory.StoragePolicyClient
	// RequestClient Creates a new request.Client instance for HTTP requests.
	RequestClient(opts ...request.Option) request.Client
	// ShareClient Creates a new inventory.ShareClient instance for access DB share store.
	ShareClient() inventory.ShareClient
	// TaskClient Creates a new inventory.TaskClient instance for access DB task store.
	TaskClient() inventory.TaskClient
	// ForkWithLogger create a shallow copy of dependency with a new correlated logger, used as per-request dep.
	ForkWithLogger(ctx context.Context, l logging.Logger) context.Context
	// MediaMetaQueue Get a singleton queue.Queue instance for media metadata processing.
	MediaMetaQueue(ctx context.Context) queue.Queue
	// SlaveQueue Get a singleton queue.Queue instance for slave tasks.
	SlaveQueue(ctx context.Context) queue.Queue
	// MediaMetaExtractor Get a singleton mediameta.Extractor instance for media metadata extraction.
	MediaMetaExtractor(ctx context.Context) mediameta.Extractor
	// ThumbPipeline Get a singleton thumb.Generator instance for chained thumbnail generation.
	ThumbPipeline() thumb.Generator
	// ThumbQueue Get a singleton queue.Queue instance for thumbnail generation.
	ThumbQueue(ctx context.Context) queue.Queue
	// EntityRecycleQueue Get a singleton queue.Queue instance for entity recycle.
	EntityRecycleQueue(ctx context.Context) queue.Queue
	// MimeDetector Get a singleton fs.MimeDetector instance for MIME type detection.
	MimeDetector(ctx context.Context) mime.MimeDetector
	// CredManager Get a singleton credmanager.CredManager instance for credential management.
	CredManager() credmanager.CredManager
	// IoIntenseQueue Get a singleton queue.Queue instance for IO intense tasks.
	IoIntenseQueue(ctx context.Context) queue.Queue
	// RemoteDownloadQueue Get a singleton queue.Queue instance for remote download tasks.
	RemoteDownloadQueue(ctx context.Context) queue.Queue
	// NodePool Get a singleton cluster.NodePool instance for node pool management.
	NodePool(ctx context.Context) (cluster.NodePool, error)
	// TaskRegistry Get a singleton queue.TaskRegistry instance for task registration.
	TaskRegistry() queue.TaskRegistry
	// WebAuthn Get a singleton webauthn.WebAuthn instance for WebAuthn authentication.
	WebAuthn(ctx context.Context) (*webauthn.WebAuthn, error)
	// UAParser Get a singleton uaparser.Parser instance for user agent parsing.
	UAParser() *uaparser.Parser
	// MasterEncryptKeyVault Get a singleton encrypt.MasterEncryptKeyVault instance for master encrypt key vault.
	MasterEncryptKeyVault(ctx context.Context) encrypt.MasterEncryptKeyVault
	// EncryptorFactory Get a new encrypt.CryptorFactory instance.
	EncryptorFactory(ctx context.Context) encrypt.CryptorFactory
	// EventHub Get a singleton eventhub.EventHub instance for event publishing.
	EventHub() eventhub.EventHub
}

type dependency struct {
	configProvider        conf.ConfigProvider
	logger                logging.Logger
	statics               iofs.FS
	serverStaticFS        static.ServeFileSystem
	dbClient              *ent.Client
	rawEntClient          *ent.Client
	kv                    cache.Driver
	navigatorStateKv      cache.Driver
	settingClient         inventory.SettingClient
	fileClient            inventory.FileClient
	shareClient           inventory.ShareClient
	settingProvider       setting.Provider
	userClient            inventory.UserClient
	groupClient           inventory.GroupClient
	storagePolicyClient   inventory.StoragePolicyClient
	taskClient            inventory.TaskClient
	nodeClient            inventory.NodeClient
	davAccountClient      inventory.DavAccountClient
	directLinkClient      inventory.DirectLinkClient
	fsEventClient         inventory.FsEventClient
	oAuthClient           inventory.OAuthClientClient
	emailClient           email.Driver
	generalAuth           auth.Auth
	hashidEncoder         hashid.Encoder
	tokenAuth             auth.TokenAuth
	lockSystem            lock.LockSystem
	requestClient         request.Client
	ioIntenseQueue        queue.Queue
	thumbQueue            queue.Queue
	mediaMetaQueue        queue.Queue
	entityRecycleQueue    queue.Queue
	slaveQueue            queue.Queue
	remoteDownloadQueue   queue.Queue
	ioIntenseQueueTask    queue.Task
	mediaMeta             mediameta.Extractor
	thumbPipeline         thumb.Generator
	mimeDetector          mime.MimeDetector
	credManager           credmanager.CredManager
	nodePool              cluster.NodePool
	taskRegistry          queue.TaskRegistry
	webauthn              *webauthn.WebAuthn
	parser                *uaparser.Parser
	cron                  *cron.Cron
	masterEncryptKeyVault encrypt.MasterEncryptKeyVault
	eventHub              eventhub.EventHub

	configPath        string
	isPro             bool
	requiredDbVersion string

	// Protects inner deps that can be reloaded at runtime.
	mu sync.Mutex
}

// NewDependency creates a new Dep instance for construct dependencies.
func NewDependency(opts ...Option) Dep {
	d := &dependency{}
	for _, o := range opts {
		o.apply(d)
	}

	return d
}

// FromContext retrieves a Dep instance from context.
func FromContext(ctx context.Context) Dep {
	return ctx.Value(DepCtx{}).(Dep)
}

func (d *dependency) RequestClient(opts ...request.Option) request.Client {
	if d.requestClient != nil {
		return d.requestClient
	}

	return request.NewClient(d.ConfigProvider(), opts...)
}

func (d *dependency) MasterEncryptKeyVault(ctx context.Context) encrypt.MasterEncryptKeyVault {
	if d.masterEncryptKeyVault != nil {
		return d.masterEncryptKeyVault
	}

	d.masterEncryptKeyVault = encrypt.NewMasterEncryptKeyVault(ctx, d.SettingProvider())
	return d.masterEncryptKeyVault
}

func (d *dependency) EncryptorFactory(ctx context.Context) encrypt.CryptorFactory {
	return encrypt.NewCryptorFactory(d.MasterEncryptKeyVault(ctx))
}

func (d *dependency) WebAuthn(ctx context.Context) (*webauthn.WebAuthn, error) {
	if d.webauthn != nil {
		return d.webauthn, nil
	}

	settings := d.SettingProvider()
	siteBasic := settings.SiteBasic(ctx)
	wConfig := &webauthn.Config{
		RPDisplayName: siteBasic.Name,
		RPID:          settings.SiteURL(ctx).Hostname(),
		RPOrigins: lo.Map(settings.AllSiteURLs(ctx), func(item *url.URL, index int) string {
			item.Path = ""
			return item.String()
		}), // The origin URLs allowed for WebAuthn requests
	}

	return webauthn.New(wConfig)
}

func (d *dependency) UAParser() *uaparser.Parser {
	if d.parser != nil {
		return d.parser
	}

	d.parser = uaparser.NewFromSaved()
	return d.parser
}

func (d *dependency) ConfigProvider() conf.ConfigProvider {
	if d.configProvider != nil {
		return d.configProvider
	}

	if d.configPath == "" {
		d.panicError(ErrorConfigPathNotSet)
	}

	var err error
	d.configProvider, err = conf.NewIniConfigProvider(d.configPath, logging.NewConsoleLogger(logging.LevelInformational))
	if err != nil {
		d.panicError(err)
	}

	return d.configProvider
}

func (d *dependency) Logger() logging.Logger {
	if d.logger != nil {
		return d.logger
	}

	config := d.ConfigProvider()
	logLevel := logging.LogLevel(config.System().LogLevel)
	if config.System().Debug {
		logLevel = logging.LevelDebug
	}

	d.logger = logging.NewConsoleLogger(logLevel)
	d.logger.Info("Logger initialized with LogLevel=%q.", logLevel)
	return d.logger
}

func (d *dependency) Statics() iofs.FS {
	if d.statics != nil {
		return d.statics
	}

	d.statics = statics.NewStaticFS(d.Logger())
	return d.statics
}

func (d *dependency) ServerStaticFS() static.ServeFileSystem {
	if d.serverStaticFS != nil {
		return d.serverStaticFS
	}

	sfs, err := statics.NewServerStaticFS(d.Logger(), d.Statics(), d.isPro)
	if err != nil {
		d.panicError(err)
	}

	d.serverStaticFS = sfs
	return d.serverStaticFS
}

func (d *dependency) DBClient() *ent.Client {
	if d.dbClient != nil {
		return d.dbClient
	}

	if d.rawEntClient == nil {
		client, err := inventory.NewRawEntClient(d.Logger(), d.ConfigProvider())
		if err != nil {
			d.panicError(err)
		}

		d.rawEntClient = client
	}

	proSuffix := ""
	if d.isPro {
		proSuffix = "-pro"
	}

	client, err := inventory.InitializeDBClient(d.Logger(), d.rawEntClient, d.KV(), d.requiredDbVersion+proSuffix)
	if err != nil {
		d.panicError(err)
	}

	d.dbClient = client
	return d.dbClient
}

func (d *dependency) KV() cache.Driver {
	if d.kv != nil {
		return d.kv
	}

	config := d.ConfigProvider().Redis()
	if config.Server != "" {
		d.kv = cache.NewRedisStore(
			d.Logger(),
			10,
			config,
		)
	} else {
		d.kv = cache.NewMemoStore(util.DataPath(cache.DefaultCacheFile), d.Logger())
	}

	return d.kv
}

func (d *dependency) NavigatorStateKV() cache.Driver {
	if d.navigatorStateKv != nil {
		return d.navigatorStateKv
	}
	d.navigatorStateKv = cache.NewMemoStore("", d.Logger())
	return d.navigatorStateKv
}

func (d *dependency) EventHub() eventhub.EventHub {
	if d.eventHub != nil {
		return d.eventHub
	}
	d.eventHub = eventhub.NewEventHub(d.UserClient(), d.FsEventClient(), d.SettingProvider())
	return d.eventHub
}

func (d *dependency) FsEventClient() inventory.FsEventClient {
	if d.fsEventClient != nil {
		return d.fsEventClient
	}
	return inventory.NewFsEventClient(d.DBClient(), d.ConfigProvider().Database().Type)
}

func (d *dependency) SettingClient() inventory.SettingClient {
	if d.settingClient != nil {
		return d.settingClient
	}

	d.settingClient = inventory.NewSettingClient(d.DBClient(), d.KV())
	return d.settingClient
}

func (d *dependency) SettingProvider() setting.Provider {
	if d.settingProvider != nil {
		return d.settingProvider
	}

	if d.ConfigProvider().System().Mode == conf.MasterMode {
		// For master mode, setting value will be retrieved in order:
		// Env overwrite -> KV Store -> DB Setting Store
		d.settingProvider = setting.NewProvider(
			setting.NewEnvOverrideStore(
				setting.NewKvSettingStore(d.KV(),
					setting.NewDbSettingStore(d.SettingClient(), nil),
				),
				d.Logger(),
			),
		)
	} else {
		// For slave mode, setting value will be retrieved in order:
		// Env overwrite -> Config file overwrites -> Setting defaults in DB schema
		d.settingProvider = setting.NewProvider(
			setting.NewEnvOverrideStore(
				setting.NewConfSettingStore(d.ConfigProvider(),
					setting.NewDbDefaultStore(nil),
				),
				d.Logger(),
			),
		)
	}

	return d.settingProvider
}

func (d *dependency) UserClient() inventory.UserClient {
	if d.userClient != nil {
		return d.userClient
	}

	return inventory.NewUserClient(d.DBClient())
}

func (d *dependency) GroupClient() inventory.GroupClient {
	if d.groupClient != nil {
		return d.groupClient
	}

	return inventory.NewGroupClient(d.DBClient(), d.ConfigProvider().Database().Type, d.KV())
}

func (d *dependency) NodeClient() inventory.NodeClient {
	if d.nodeClient != nil {
		return d.nodeClient
	}

	return inventory.NewNodeClient(d.DBClient())
}

func (d *dependency) NodePool(ctx context.Context) (cluster.NodePool, error) {
	reload, _ := ctx.Value(ReloadCtx{}).(bool)
	if d.nodePool != nil && !reload {
		return d.nodePool, nil
	}

	if d.ConfigProvider().System().Mode == conf.MasterMode {
		np, err := cluster.NewNodePool(ctx, d.Logger(), d.ConfigProvider(), d.SettingProvider(), d.NodeClient())
		if err != nil {
			return nil, err
		}

		d.nodePool = np
	} else {
		d.nodePool = cluster.NewSlaveDummyNodePool(ctx, d.ConfigProvider(), d.SettingProvider())
	}

	return d.nodePool, nil
}

func (d *dependency) EmailClient(ctx context.Context) email.Driver {
	d.mu.Lock()
	defer d.mu.Unlock()

	if reload, _ := ctx.Value(ReloadCtx{}).(bool); reload || d.emailClient == nil {
		if d.emailClient != nil {
			d.emailClient.Close()
		}
		d.emailClient = email.NewSMTPPool(d.SettingProvider(), d.Logger())
	}

	return d.emailClient
}

func (d *dependency) OAuthClientClient() inventory.OAuthClientClient {
	if d.oAuthClient != nil {
		return d.oAuthClient
	}

	return inventory.NewOAuthClientClient(d.DBClient(), d.ConfigProvider().Database().Type)
}

func (d *dependency) MimeDetector(ctx context.Context) mime.MimeDetector {
	d.mu.Lock()
	defer d.mu.Unlock()

	_, reload := ctx.Value(ReloadCtx{}).(bool)
	if d.mimeDetector != nil && !reload {
		return d.mimeDetector
	}

	d.mimeDetector = mime.NewMimeDetector(ctx, d.SettingProvider(), d.Logger())
	return d.mimeDetector
}

func (d *dependency) MediaMetaExtractor(ctx context.Context) mediameta.Extractor {
	d.mu.Lock()
	defer d.mu.Unlock()

	_, reload := ctx.Value(ReloadCtx{}).(bool)
	if d.mediaMeta != nil && !reload {
		return d.mediaMeta
	}

	d.mediaMeta = mediameta.NewExtractorManager(ctx, d.SettingProvider(), d.Logger(), d.RequestClient())
	return d.mediaMeta
}

func (d *dependency) ThumbQueue(ctx context.Context) queue.Queue {
	d.mu.Lock()
	defer d.mu.Unlock()

	_, reload := ctx.Value(ReloadCtx{}).(bool)
	if d.thumbQueue != nil && !reload {
		return d.thumbQueue
	}

	if d.thumbQueue != nil {
		d.thumbQueue.Shutdown()
	}

	settings := d.SettingProvider()
	queueSetting := settings.Queue(context.Background(), setting.QueueTypeThumb)
	var (
		t inventory.TaskClient
	)
	if d.ConfigProvider().System().Mode == conf.MasterMode {
		t = d.TaskClient()
	}

	d.thumbQueue = queue.New(d.Logger(), t, nil, d,
		queue.WithBackoffFactor(queueSetting.BackoffFactor),
		queue.WithMaxRetry(queueSetting.MaxRetry),
		queue.WithBackoffMaxDuration(queueSetting.BackoffMaxDuration),
		queue.WithRetryDelay(queueSetting.RetryDelay),
		queue.WithWorkerCount(queueSetting.WorkerNum),
		queue.WithName("ThumbQueue"),
		queue.WithMaxTaskExecution(queueSetting.MaxExecution),
	)
	return d.thumbQueue
}

func (d *dependency) MediaMetaQueue(ctx context.Context) queue.Queue {
	d.mu.Lock()
	defer d.mu.Unlock()

	_, reload := ctx.Value(ReloadCtx{}).(bool)
	if d.mediaMetaQueue != nil && !reload {
		return d.mediaMetaQueue
	}

	if d.mediaMetaQueue != nil {
		d.mediaMetaQueue.Shutdown()
	}

	settings := d.SettingProvider()
	queueSetting := settings.Queue(context.Background(), setting.QueueTypeMediaMeta)

	d.mediaMetaQueue = queue.New(d.Logger(), d.TaskClient(), nil, d,
		queue.WithBackoffFactor(queueSetting.BackoffFactor),
		queue.WithMaxRetry(queueSetting.MaxRetry),
		queue.WithBackoffMaxDuration(queueSetting.BackoffMaxDuration),
		queue.WithRetryDelay(queueSetting.RetryDelay),
		queue.WithWorkerCount(queueSetting.WorkerNum),
		queue.WithName("MediaMetadataQueue"),
		queue.WithMaxTaskExecution(queueSetting.MaxExecution),
		queue.WithResumeTaskType(queue.MediaMetaTaskType),
	)
	return d.mediaMetaQueue
}

func (d *dependency) IoIntenseQueue(ctx context.Context) queue.Queue {
	d.mu.Lock()
	defer d.mu.Unlock()

	_, reload := ctx.Value(ReloadCtx{}).(bool)
	if d.ioIntenseQueue != nil && !reload {
		return d.ioIntenseQueue
	}

	if d.ioIntenseQueue != nil {
		d.ioIntenseQueue.Shutdown()
	}

	settings := d.SettingProvider()
	queueSetting := settings.Queue(context.Background(), setting.QueueTypeIOIntense)

	d.ioIntenseQueue = queue.New(d.Logger(), d.TaskClient(), d.TaskRegistry(), d,
		queue.WithBackoffFactor(queueSetting.BackoffFactor),
		queue.WithMaxRetry(queueSetting.MaxRetry),
		queue.WithBackoffMaxDuration(queueSetting.BackoffMaxDuration),
		queue.WithRetryDelay(queueSetting.RetryDelay),
		queue.WithWorkerCount(queueSetting.WorkerNum),
		queue.WithName("IoIntenseQueue"),
		queue.WithMaxTaskExecution(queueSetting.MaxExecution),
		queue.WithResumeTaskType(queue.CreateArchiveTaskType, queue.ExtractArchiveTaskType, queue.RelocateTaskType, queue.ImportTaskType),
		queue.WithTaskPullInterval(10*time.Second),
	)
	return d.ioIntenseQueue
}

func (d *dependency) RemoteDownloadQueue(ctx context.Context) queue.Queue {
	d.mu.Lock()
	defer d.mu.Unlock()

	_, reload := ctx.Value(ReloadCtx{}).(bool)
	if d.remoteDownloadQueue != nil && !reload {
		return d.remoteDownloadQueue
	}

	if d.remoteDownloadQueue != nil {
		d.remoteDownloadQueue.Shutdown()
	}

	settings := d.SettingProvider()
	queueSetting := settings.Queue(context.Background(), setting.QueueTypeRemoteDownload)

	d.remoteDownloadQueue = queue.New(d.Logger(), d.TaskClient(), d.TaskRegistry(), d,
		queue.WithBackoffFactor(queueSetting.BackoffFactor),
		queue.WithMaxRetry(queueSetting.MaxRetry),
		queue.WithBackoffMaxDuration(queueSetting.BackoffMaxDuration),
		queue.WithRetryDelay(queueSetting.RetryDelay),
		queue.WithWorkerCount(queueSetting.WorkerNum),
		queue.WithName("RemoteDownloadQueue"),
		queue.WithMaxTaskExecution(queueSetting.MaxExecution),
		queue.WithResumeTaskType(queue.RemoteDownloadTaskType),
		queue.WithTaskPullInterval(10*time.Second),
	)
	return d.remoteDownloadQueue
}

func (d *dependency) EntityRecycleQueue(ctx context.Context) queue.Queue {
	d.mu.Lock()
	defer d.mu.Unlock()

	_, reload := ctx.Value(ReloadCtx{}).(bool)
	if d.entityRecycleQueue != nil && !reload {
		return d.entityRecycleQueue
	}

	if d.entityRecycleQueue != nil {
		d.entityRecycleQueue.Shutdown()
	}

	settings := d.SettingProvider()
	queueSetting := settings.Queue(context.Background(), setting.QueueTypeEntityRecycle)

	d.entityRecycleQueue = queue.New(d.Logger(), d.TaskClient(), nil, d,
		queue.WithBackoffFactor(queueSetting.BackoffFactor),
		queue.WithMaxRetry(queueSetting.MaxRetry),
		queue.WithBackoffMaxDuration(queueSetting.BackoffMaxDuration),
		queue.WithRetryDelay(queueSetting.RetryDelay),
		queue.WithWorkerCount(queueSetting.WorkerNum),
		queue.WithName("EntityRecycleQueue"),
		queue.WithMaxTaskExecution(queueSetting.MaxExecution),
		queue.WithResumeTaskType(queue.EntityRecycleRoutineTaskType, queue.ExplicitEntityRecycleTaskType, queue.UploadSentinelCheckTaskType),
		queue.WithTaskPullInterval(10*time.Second),
	)
	return d.entityRecycleQueue
}

func (d *dependency) SlaveQueue(ctx context.Context) queue.Queue {
	d.mu.Lock()
	defer d.mu.Unlock()

	_, reload := ctx.Value(ReloadCtx{}).(bool)
	if d.slaveQueue != nil && !reload {
		return d.slaveQueue
	}

	if d.slaveQueue != nil {
		d.slaveQueue.Shutdown()
	}

	settings := d.SettingProvider()
	queueSetting := settings.Queue(context.Background(), setting.QueueTypeSlave)

	d.slaveQueue = queue.New(d.Logger(), nil, nil, d,
		queue.WithBackoffFactor(queueSetting.BackoffFactor),
		queue.WithMaxRetry(queueSetting.MaxRetry),
		queue.WithBackoffMaxDuration(queueSetting.BackoffMaxDuration),
		queue.WithRetryDelay(queueSetting.RetryDelay),
		queue.WithWorkerCount(queueSetting.WorkerNum),
		queue.WithName("SlaveQueue"),
		queue.WithMaxTaskExecution(queueSetting.MaxExecution),
	)
	return d.slaveQueue
}

func (d *dependency) GeneralAuth() auth.Auth {
	if d.generalAuth != nil {
		return d.generalAuth
	}

	var secretKey string
	if d.ConfigProvider().System().Mode == conf.MasterMode {
		secretKey = d.SettingProvider().SecretKey(context.Background())
	} else {
		secretKey = d.ConfigProvider().Slave().Secret
		if secretKey == "" {
			d.panicError(errors.New("SlaveSecret is not set, please specify it in config file"))
		}
	}

	d.generalAuth = auth.HMACAuth{
		SecretKey: []byte(secretKey),
	}

	return d.generalAuth
}

func (d *dependency) FileClient() inventory.FileClient {
	if d.fileClient != nil {
		return d.fileClient
	}

	return inventory.NewFileClient(d.DBClient(), d.ConfigProvider().Database().Type, d.HashIDEncoder())
}

func (d *dependency) ShareClient() inventory.ShareClient {
	if d.shareClient != nil {
		return d.shareClient
	}

	return inventory.NewShareClient(d.DBClient(), d.ConfigProvider().Database().Type, d.HashIDEncoder())
}

func (d *dependency) TaskClient() inventory.TaskClient {
	if d.taskClient != nil {
		return d.taskClient
	}

	return inventory.NewTaskClient(d.DBClient(), d.ConfigProvider().Database().Type, d.HashIDEncoder())
}

func (d *dependency) DavAccountClient() inventory.DavAccountClient {
	if d.davAccountClient != nil {
		return d.davAccountClient
	}

	return inventory.NewDavAccountClient(d.DBClient(), d.ConfigProvider().Database().Type, d.HashIDEncoder())
}

func (d *dependency) DirectLinkClient() inventory.DirectLinkClient {
	if d.directLinkClient != nil {
		return d.directLinkClient
	}

	return inventory.NewDirectLinkClient(d.DBClient(), d.ConfigProvider().Database().Type, d.HashIDEncoder())
}

func (d *dependency) HashIDEncoder() hashid.Encoder {
	if d.hashidEncoder != nil {
		return d.hashidEncoder
	}

	encoder, err := hashid.New(d.SettingProvider().HashIDSalt(context.Background()))
	if err != nil {
		d.panicError(err)
	}

	d.hashidEncoder = encoder
	return d.hashidEncoder
}

func (d *dependency) CredManager() credmanager.CredManager {
	if d.credManager != nil {
		return d.credManager
	}

	if d.ConfigProvider().System().Mode == conf.MasterMode {
		d.credManager = credmanager.New(d.KV())
	} else {
		d.credManager = credmanager.NewSlaveManager(d.KV(), d.ConfigProvider())
	}
	return d.credManager
}

func (d *dependency) TokenAuth() auth.TokenAuth {
	if d.tokenAuth != nil {
		return d.tokenAuth
	}

	d.tokenAuth = auth.NewTokenAuth(d.HashIDEncoder(), d.SettingProvider(),
		[]byte(d.SettingProvider().SecretKey(context.Background())), d.UserClient(), d.Logger(), d.KV(), d.OAuthClientClient())
	return d.tokenAuth
}

func (d *dependency) LockSystem() lock.LockSystem {
	if d.lockSystem != nil {
		return d.lockSystem
	}

	d.lockSystem = lock.NewMemLS(d.HashIDEncoder(), d.Logger())
	return d.lockSystem
}

func (d *dependency) StoragePolicyClient() inventory.StoragePolicyClient {
	if d.storagePolicyClient != nil {
		return d.storagePolicyClient
	}

	return inventory.NewStoragePolicyClient(d.DBClient(), d.KV())
}

func (d *dependency) ThumbPipeline() thumb.Generator {
	if d.thumbPipeline != nil {
		return d.thumbPipeline
	}

	d.thumbPipeline = thumb.NewPipeline(d.SettingProvider(), d.Logger())
	return d.thumbPipeline
}

func (d *dependency) TaskRegistry() queue.TaskRegistry {
	if d.taskRegistry != nil {
		return d.taskRegistry
	}

	d.taskRegistry = queue.NewTaskRegistry()
	return d.taskRegistry
}

func (d *dependency) Shutdown(ctx context.Context) error {
	d.mu.Lock()

	if d.emailClient != nil {
		d.emailClient.Close()
	}

	wg := sync.WaitGroup{}

	if d.mediaMetaQueue != nil {
		wg.Add(1)
		go func() {
			d.mediaMetaQueue.Shutdown()
			defer wg.Done()
		}()
	}

	if d.thumbQueue != nil {
		wg.Add(1)
		go func() {
			d.thumbQueue.Shutdown()
			defer wg.Done()
		}()
	}

	if d.ioIntenseQueue != nil {
		wg.Add(1)
		go func() {
			d.ioIntenseQueue.Shutdown()
			defer wg.Done()
		}()
	}

	if d.entityRecycleQueue != nil {
		wg.Add(1)
		go func() {
			d.entityRecycleQueue.Shutdown()
			defer wg.Done()
		}()
	}

	if d.slaveQueue != nil {
		wg.Add(1)
		go func() {
			d.slaveQueue.Shutdown()
			defer wg.Done()
		}()
	}

	if d.remoteDownloadQueue != nil {
		wg.Add(1)
		go func() {
			d.remoteDownloadQueue.Shutdown()
			defer wg.Done()
		}()
	}

	if d.eventHub != nil {
		wg.Add(1)
		go func() {
			d.eventHub.Close()
			defer wg.Done()
		}()
	}

	d.mu.Unlock()
	wg.Wait()

	return nil
}

func (d *dependency) panicError(err error) {
	if d.logger != nil {
		d.logger.Panic("Fatal error in dependency initialization: %s", err)
	}

	panic(err)
}

func (d *dependency) ForkWithLogger(ctx context.Context, l logging.Logger) context.Context {
	dep := &dependencyCorrelated{
		l:          l,
		dependency: d,
	}
	return context.WithValue(ctx, DepCtx{}, dep)
}

type dependencyCorrelated struct {
	l logging.Logger
	*dependency
}

func (d *dependencyCorrelated) Logger() logging.Logger {
	return d.l
}
