package core

import (
	"os"
	"runtime"
	"seanime/internal/api/anilist"
	"seanime/internal/api/metadata"
	"seanime/internal/constants"
	"seanime/internal/continuity"
	"seanime/internal/database/db"
	"seanime/internal/database/models"
	debrid_client "seanime/internal/debrid/client"
	"seanime/internal/directstream"
	discordrpc_presence "seanime/internal/discordrpc/presence"
	"seanime/internal/doh"
	"seanime/internal/events"
	"seanime/internal/extension_playground"
	"seanime/internal/extension_repo"
	"seanime/internal/hook"
	"seanime/internal/library/autodownloader"
	"seanime/internal/library/autoscanner"
	"seanime/internal/library/fillermanager"
	"seanime/internal/library/playbackmanager"
	"seanime/internal/library/scanner"
	"seanime/internal/local"
	"seanime/internal/manga"
	"seanime/internal/mediaplayers/iina"
	"seanime/internal/mediaplayers/mediaplayer"
	"seanime/internal/mediaplayers/mpchc"
	"seanime/internal/mediaplayers/mpv"
	"seanime/internal/mediaplayers/vlc"
	"seanime/internal/mediastream"
	"seanime/internal/nakama"
	"seanime/internal/nativeplayer"
	"seanime/internal/onlinestream"
	"seanime/internal/platforms/anilist_platform"
	"seanime/internal/platforms/offline_platform"
	"seanime/internal/platforms/platform"
	"seanime/internal/platforms/simulated_platform"
	"seanime/internal/plugin"
	"seanime/internal/report"
	"seanime/internal/torrent_clients/torrent_client"
	"seanime/internal/torrents/torrent"
	"seanime/internal/torrentstream"
	"seanime/internal/updater"
	"seanime/internal/user"
	"seanime/internal/util"
	"seanime/internal/util/filecache"
	"seanime/internal/util/result"
	"sync"

	"github.com/rs/zerolog"
)

type (
	App struct {
		Config                        *Config
		Database                      *db.Database
		Logger                        *zerolog.Logger
		TorrentClientRepository       *torrent_client.Repository
		TorrentRepository             *torrent.Repository
		DebridClientRepository        *debrid_client.Repository
		Watcher                       *scanner.Watcher
		AnilistClient                 anilist.AnilistClient
		AnilistPlatform               platform.Platform
		OfflinePlatform               platform.Platform
		LocalManager                  local.Manager
		FillerManager                 *fillermanager.FillerManager
		WSEventManager                *events.WSEventManager
		AutoDownloader                *autodownloader.AutoDownloader
		ExtensionRepository           *extension_repo.Repository
		ExtensionPlaygroundRepository *extension_playground.PlaygroundRepository
		DirectStreamManager           *directstream.Manager
		NativePlayer                  *nativeplayer.NativePlayer
		MediaPlayer                   struct {
			VLC   *vlc.VLC
			MpcHc *mpchc.MpcHc
			Mpv   *mpv.Mpv
			Iina  *iina.Iina
		}
		MediaPlayerRepository           *mediaplayer.Repository
		Version                         string
		Updater                         *updater.Updater
		AutoScanner                     *autoscanner.AutoScanner
		PlaybackManager                 *playbackmanager.PlaybackManager
		FileCacher                      *filecache.Cacher
		OnlinestreamRepository          *onlinestream.Repository
		MangaRepository                 *manga.Repository
		MetadataProvider                metadata.Provider
		DiscordPresence                 *discordrpc_presence.Presence
		MangaDownloader                 *manga.Downloader
		ContinuityManager               *continuity.Manager
		Cleanups                        []func()
		OnRefreshAnilistCollectionFuncs *result.Map[string, func()]
		OnFlushLogs                     func()
		MediastreamRepository           *mediastream.Repository
		TorrentstreamRepository         *torrentstream.Repository
		FeatureFlags                    FeatureFlags
		Settings                        *models.Settings
		SecondarySettings               struct {
			Mediastream   *models.MediastreamSettings
			Torrentstream *models.TorrentstreamSettings
			Debrid        *models.DebridSettings
		} // Struct for other settings sent to clientN
		SelfUpdater        *updater.SelfUpdater
		ReportRepository   *report.Repository
		TotalLibrarySize   uint64 // Initialized in modules.go
		LibraryDir         string
		IsDesktopSidecar   bool
		animeCollection    *anilist.AnimeCollection
		rawAnimeCollection *anilist.AnimeCollection // (retains custom lists)
		mangaCollection    *anilist.MangaCollection
		rawMangaCollection *anilist.MangaCollection // (retains custom lists)
		user               *user.User
		previousVersion    string
		moduleMu           sync.Mutex
		HookManager        hook.Manager
		ServerReady        bool // Whether the Anilist data from the first request has been fetched
		isOffline          *bool
		NakamaManager      *nakama.Manager
		ServerPasswordHash string // SHA-256 hash of the server password
	}
)

// NewApp creates a new server instance
func NewApp(configOpts *ConfigOptions, selfupdater *updater.SelfUpdater) *App {

	// Initialize logger with predefined format
	logger := util.NewLogger()

	// Log application version, OS, architecture and system info
	logger.Info().Msgf("app: Seanime %s-%s", constants.Version, constants.VersionName)
	logger.Info().Msgf("app: OS: %s", runtime.GOOS)
	logger.Info().Msgf("app: Arch: %s", runtime.GOARCH)
	logger.Info().Msgf("app: Processor count: %d", runtime.NumCPU())

	// Initialize hook manager for plugin event system
	hookManager := hook.NewHookManager(hook.NewHookManagerOptions{Logger: logger})
	hook.SetGlobalHookManager(hookManager)
	plugin.GlobalAppContext.SetLogger(logger)

	// Store current version to detect version changes
	previousVersion := constants.Version

	// Add callback to track version changes
	configOpts.OnVersionChange = append(configOpts.OnVersionChange, func(oldVersion string, newVersion string) {
		logger.Info().Str("prev", oldVersion).Str("current", newVersion).Msg("app: Version change detected")
		previousVersion = oldVersion
	})

	// Initialize configuration with provided options
	// Creates config directory if it doesn't exist
	cfg, err := NewConfig(configOpts, logger)
	if err != nil {
		logger.Fatal().Err(err).Msgf("app: Failed to initialize config")
	}

	// Compute SHA-256 hash of the server password
	serverPasswordHash := ""
	if cfg.Server.Password != "" {
		serverPasswordHash = util.HashSHA256Hex(cfg.Server.Password)
	}

	// Create logs directory if it doesn't exist
	_ = os.MkdirAll(cfg.Logs.Dir, 0755)

	// Start background process to trim log files
	go TrimLogEntries(cfg.Logs.Dir, logger)

	logger.Info().Msgf("app: Data directory: %s", cfg.Data.AppDataDir)
	logger.Info().Msgf("app: Working directory: %s", cfg.Data.WorkingDir)

	// Log if running in desktop sidecar mode
	if configOpts.IsDesktopSidecar {
		logger.Info().Msg("app: Desktop sidecar mode enabled")
	}

	// Initialize database connection
	database, err := db.NewDatabase(cfg.Data.AppDataDir, cfg.Database.Name, logger)
	if err != nil {
		logger.Fatal().Err(err).Msgf("app: Failed to initialize database")
	}

	HandleNewDatabaseEntries(database, logger)

	// Clean up old database entries in background goroutines
	database.TrimLocalFileEntries()     // Remove old local file entries
	database.TrimScanSummaryEntries()   // Remove old scan summaries
	database.TrimTorrentstreamHistory() // Remove old torrent stream history

	// Get anime library paths for plugin context
	animeLibraryPaths, _ := database.GetAllLibraryPathsFromSettings()
	plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
		Database:          database,
		AnimeLibraryPaths: &animeLibraryPaths,
	})

	// Get Anilist token from database if available
	anilistToken := database.GetAnilistToken()

	// Initialize Anilist API client with the token
	// If the token is empty, the client will not be authenticated
	anilistCW := anilist.NewAnilistClient(anilistToken)

	// Initialize WebSocket event manager for real-time communication
	wsEventManager := events.NewWSEventManager(logger)

	// Exit if no WebSocket connections in desktop sidecar mode
	if configOpts.IsDesktopSidecar {
		wsEventManager.ExitIfNoConnsAsDesktopSidecar()
	}

	// Initialize DNS-over-HTTPS service in background
	go doh.HandleDoH(cfg.Server.DoHUrl, logger)

	// Initialize file cache system for media and metadata
	fileCacher, err := filecache.NewCacher(cfg.Cache.Dir)
	if err != nil {
		logger.Fatal().Err(err).Msgf("app: Failed to initialize file cacher")
	}

	// Initialize extension repository
	extensionRepository := extension_repo.NewRepository(&extension_repo.NewRepositoryOptions{
		Logger:         logger,
		ExtensionDir:   cfg.Extensions.Dir,
		WSEventManager: wsEventManager,
		FileCacher:     fileCacher,
		HookManager:    hookManager,
	})
	// Load extensions in background
	go LoadExtensions(extensionRepository, logger, cfg)

	// Initialize metadata provider for media information
	metadataProvider := metadata.NewProvider(&metadata.NewProviderImplOptions{
		Logger:     logger,
		FileCacher: fileCacher,
	})

	// Set initial metadata provider (will change if offline mode is enabled)
	activeMetadataProvider := metadataProvider

	// Initialize manga repository
	mangaRepository := manga.NewRepository(&manga.NewRepositoryOptions{
		Logger:         logger,
		FileCacher:     fileCacher,
		CacheDir:       cfg.Cache.Dir,
		ServerURI:      cfg.GetServerURI(),
		WsEventManager: wsEventManager,
		DownloadDir:    cfg.Manga.DownloadDir,
		Database:       database,
	})

	// Initialize Anilist platform
	anilistPlatform := anilist_platform.NewAnilistPlatform(anilistCW, logger)

	// Update plugin context with new modules
	plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
		AnilistPlatform:  anilistPlatform,
		WSEventManager:   wsEventManager,
		MetadataProvider: metadataProvider,
	})

	// Initialize sync manager for offline/online synchronization
	localManager, err := local.NewManager(&local.NewManagerOptions{
		LocalDir:         cfg.Offline.Dir,
		AssetDir:         cfg.Offline.AssetDir,
		Logger:           logger,
		MetadataProvider: metadataProvider,
		MangaRepository:  mangaRepository,
		Database:         database,
		WSEventManager:   wsEventManager,
		IsOffline:        cfg.Server.Offline,
		AnilistPlatform:  anilistPlatform,
	})
	if err != nil {
		logger.Fatal().Err(err).Msgf("app: Failed to initialize sync manager")
	}

	// Use local metadata provider if in offline mode
	if cfg.Server.Offline {
		activeMetadataProvider = localManager.GetOfflineMetadataProvider()
	}

	// Initialize local platform for offline operations
	offlinePlatform, err := offline_platform.NewOfflinePlatform(localManager, anilistCW, logger)
	if err != nil {
		logger.Fatal().Err(err).Msgf("app: Failed to initialize local platform")
	}

	// Initialize simulated platform for unauthenticated operations
	simulatedPlatform, err := simulated_platform.NewSimulatedPlatform(localManager, anilistCW, logger)
	if err != nil {
		logger.Fatal().Err(err).Msgf("app: Failed to initialize simulated platform")
	}

	// Change active platform if offline mode is enabled
	activePlatform := anilistPlatform
	if cfg.Server.Offline {
		activePlatform = offlinePlatform
	} else if !anilistCW.IsAuthenticated() {
		logger.Warn().Msg("app: Anilist client is not authenticated, using simulated platform")
		activePlatform = simulatedPlatform
	}

	// Initialize online streaming repository
	onlinestreamRepository := onlinestream.NewRepository(&onlinestream.NewRepositoryOptions{
		Logger:           logger,
		FileCacher:       fileCacher,
		MetadataProvider: activeMetadataProvider,
		Platform:         activePlatform,
		Database:         database,
	})

	// Initialize extension playground for testing extensions
	extensionPlaygroundRepository := extension_playground.NewPlaygroundRepository(logger, activePlatform, activeMetadataProvider)

	isOffline := cfg.Server.Offline

	// Create the main app instance with initialized components
	app := &App{
		Config:                        cfg,
		Database:                      database,
		AnilistClient:                 anilistCW,
		AnilistPlatform:               activePlatform,
		OfflinePlatform:               offlinePlatform,
		LocalManager:                  localManager,
		WSEventManager:                wsEventManager,
		Logger:                        logger,
		Version:                       constants.Version,
		Updater:                       updater.New(constants.Version, logger, wsEventManager),
		FileCacher:                    fileCacher,
		OnlinestreamRepository:        onlinestreamRepository,
		MetadataProvider:              activeMetadataProvider,
		MangaRepository:               mangaRepository,
		ExtensionRepository:           extensionRepository,
		ExtensionPlaygroundRepository: extensionPlaygroundRepository,
		ReportRepository:              report.NewRepository(logger),
		TorrentRepository:             nil, // Initialized in App.initModulesOnce
		FillerManager:                 nil, // Initialized in App.initModulesOnce
		MangaDownloader:               nil, // Initialized in App.initModulesOnce
		PlaybackManager:               nil, // Initialized in App.initModulesOnce
		AutoDownloader:                nil, // Initialized in App.initModulesOnce
		AutoScanner:                   nil, // Initialized in App.initModulesOnce
		MediastreamRepository:         nil, // Initialized in App.initModulesOnce
		TorrentstreamRepository:       nil, // Initialized in App.initModulesOnce
		ContinuityManager:             nil, // Initialized in App.initModulesOnce
		DebridClientRepository:        nil, // Initialized in App.initModulesOnce
		DirectStreamManager:           nil, // Initialized in App.initModulesOnce
		NativePlayer:                  nil, // Initialized in App.initModulesOnce
		NakamaManager:                 nil, // Initialized in App.initModulesOnce
		TorrentClientRepository:       nil, // Initialized in App.InitOrRefreshModules
		MediaPlayerRepository:         nil, // Initialized in App.InitOrRefreshModules
		DiscordPresence:               nil, // Initialized in App.InitOrRefreshModules
		previousVersion:               previousVersion,
		FeatureFlags:                  NewFeatureFlags(cfg, logger),
		IsDesktopSidecar:              configOpts.IsDesktopSidecar,
		SecondarySettings: struct {
			Mediastream   *models.MediastreamSettings
			Torrentstream *models.TorrentstreamSettings
			Debrid        *models.DebridSettings
		}{Mediastream: nil, Torrentstream: nil},
		SelfUpdater:                     selfupdater,
		moduleMu:                        sync.Mutex{},
		OnRefreshAnilistCollectionFuncs: result.NewResultMap[string, func()](),
		HookManager:                     hookManager,
		isOffline:                       &isOffline,
		ServerPasswordHash:              serverPasswordHash,
	}

	// Run database migrations if version has changed
	app.runMigrations()

	// Initialize modules that only need to be initialized once
	app.initModulesOnce()

	plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
		IsOffline:               app.IsOffline(),
		ContinuityManager:       app.ContinuityManager,
		AutoScanner:             app.AutoScanner,
		AutoDownloader:          app.AutoDownloader,
		FileCacher:              app.FileCacher,
		OnlinestreamRepository:  app.OnlinestreamRepository,
		MediastreamRepository:   app.MediastreamRepository,
		TorrentstreamRepository: app.TorrentstreamRepository,
	})

	if !*app.IsOffline() {
		go app.Updater.FetchAnnouncements()
	}

	// Initialize all modules that depend on settings
	app.InitOrRefreshModules()

	// Load built-in extensions into extension consumers
	app.AddExtensionBankToConsumers()

	// Initialize Anilist data if not in offline mode
	if !*app.IsOffline() {
		app.InitOrRefreshAnilistData()
	} else {
		app.ServerReady = true
	}

	// Initialize mediastream settings (for streaming media)
	app.InitOrRefreshMediastreamSettings()

	// Initialize torrentstream settings (for torrent streaming)
	app.InitOrRefreshTorrentstreamSettings()

	// Initialize debrid settings (for debrid services)
	app.InitOrRefreshDebridSettings()

	// Register Nakama manager cleanup
	app.AddCleanupFunction(app.NakamaManager.Cleanup)

	// Run one-time initialization actions
	app.performActionsOnce()

	return app
}

func (a *App) IsOffline() *bool {
	return a.isOffline
}

func (a *App) AddCleanupFunction(f func()) {
	a.Cleanups = append(a.Cleanups, f)
}
func (a *App) AddOnRefreshAnilistCollectionFunc(key string, f func()) {
	if key == "" {
		return
	}
	a.OnRefreshAnilistCollectionFuncs.Set(key, f)
}

func (a *App) Cleanup() {
	for _, f := range a.Cleanups {
		f()
	}
}
