package migration

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/grafana/authlib/types"
	"github.com/grafana/grafana-app-sdk/logging"
	"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)

// DefaultCacheTTL is the default TTL for the datasource and library element caches.
const DefaultCacheTTL = time.Minute

// Initialize provides the migrator singleton with required dependencies and builds the map of migrations.
func Initialize(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider, cacheTTL time.Duration) {
	migratorInstance.init(dsIndexProvider, leIndexProvider, cacheTTL)
}

// GetDataSourceIndexProvider returns the datasource index provider instance that was initialized.
func GetDataSourceIndexProvider() schemaversion.DataSourceIndexProvider {
	// Wait for initialization to complete
	<-migratorInstance.ready
	return migratorInstance.dsIndexProvider
}

// GetLibraryElementIndexProvider returns the library element index provider instance that was initialized.
func GetLibraryElementIndexProvider() schemaversion.LibraryElementIndexProvider {
	// Wait for initialization to complete
	<-migratorInstance.ready
	return migratorInstance.leIndexProvider
}

// ResetForTesting resets the migrator singleton for testing purposes.
func ResetForTesting() {
	migratorInstance = &migrator{
		migrations:      map[int]schemaversion.SchemaVersionMigrationFunc{},
		ready:           make(chan struct{}),
		dsIndexProvider: nil,
		leIndexProvider: nil,
	}
	initOnce = sync.Once{}
}

// PreloadCache preloads the datasource and library element caches for the given namespaces.
func PreloadCache(ctx context.Context, nsInfos []types.NamespaceInfo) {
	// Wait for initialization to complete
	<-migratorInstance.ready

	// Try to preload datasource cache
	if preloadable, ok := migratorInstance.dsIndexProvider.(schemaversion.PreloadableCache); ok {
		preloadable.Preload(ctx, nsInfos)
	}

	// Try to preload library element cache
	if preloadable, ok := migratorInstance.leIndexProvider.(schemaversion.PreloadableCache); ok {
		preloadable.Preload(ctx, nsInfos)
	}
}

// PreloadCacheInBackground starts a goroutine that preloads the caches for the given namespaces.
func PreloadCacheInBackground(nsInfos []types.NamespaceInfo) {
	go func() {
		defer func() {
			if r := recover(); r != nil {
				logging.DefaultLogger.Error("panic during cache preloading", "error", r)
			}
		}()
		PreloadCache(context.Background(), nsInfos)
	}()
}

// Migrate migrates the given dashboard to the target version.
// This will block until the migrator is initialized.
func Migrate(ctx context.Context, dash map[string]interface{}, targetVersion int) error {
	return migratorInstance.migrate(ctx, dash, targetVersion)
}

var (
	migratorInstance = &migrator{
		migrations: map[int]schemaversion.SchemaVersionMigrationFunc{},
		ready:      make(chan struct{}),
	}
	initOnce sync.Once
)

type migrator struct {
	ready           chan struct{}
	migrations      map[int]schemaversion.SchemaVersionMigrationFunc
	dsIndexProvider schemaversion.DataSourceIndexProvider
	leIndexProvider schemaversion.LibraryElementIndexProvider
}

func (m *migrator) init(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider, cacheTTL time.Duration) {
	initOnce.Do(func() {
		// Wrap the provider with org-aware TTL caching for all conversions.
		// This prevents repeated DB queries across multiple conversion calls while allowing
		// the cache to refresh periodically, making it suitable for long-lived singleton usage.
		m.dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider, cacheTTL)
		// Wrap library element provider with caching as well
		m.leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider, cacheTTL)
		m.migrations = schemaversion.GetMigrations(m.dsIndexProvider, m.leIndexProvider)
		close(m.ready)
	})
}

func (m *migrator) migrate(ctx context.Context, dash map[string]interface{}, targetVersion int) error {
	if dash == nil {
		return schemaversion.NewMigrationError("dashboard is nil", 0, targetVersion, "")
	}

	// wait for the migrator to be initialized
	<-m.ready

	// 0. Clean up dashboard properties that frontend never includes in save model
	// These properties are added by backend but frontend filters them out
	delete(dash, "__elements")
	delete(dash, "__inputs")
	delete(dash, "__requires")

	// 1. Track which panels had transformations in original input (before any defaults applied)
	// This is needed to match frontend hasOwnProperty behavior
	trackOriginalTransformations(dash)

	// 1.1. Track which panels had fieldConfig.defaults.custom in original input
	// This is needed to preserve empty custom objects that were originally present
	trackOriginalFieldConfigCustom(dash)

	// 2. Apply ALL frontend defaults FIRST (DashboardModel + PanelModel defaults)
	// This replicates the behavior of the frontend DashboardModel and PanelModel constructors
	applyFrontendDefaults(dash)

	// 2. Apply panel defaults to ALL panels (both top-level and nested in rows)
	// The frontend creates PanelModel instances for all panels, including those in rows
	if dashboardPanels, ok := dash["panels"].([]interface{}); ok {
		for _, panelInterface := range dashboardPanels {
			if panel, ok := panelInterface.(map[string]interface{}); ok {
				applyPanelDefaults(panel)
			}
		}
	}

	// Apply defaults to panels inside rows (for pre-v16 dashboards)
	// Match frontend upgradeToGridLayout: only panels NOT in collapsed rows get new PanelModel() constructor
	if rows, ok := dash["rows"].([]interface{}); ok {
		showRows := shouldShowRows(rows)

		for _, rowInterface := range rows {
			row, ok := rowInterface.(map[string]interface{})
			if !ok {
				continue
			}

			applyRowPanelDefaults(row, showRows)
		}
	}

	// 3. Ensure panel IDs are unique for ALL panels (including nested ones)
	// This matches the frontend EnsurePanelsHaveUniqueIds() behavior
	EnsurePanelsHaveUniqueIds(dash)

	// TODO: Probably we can check if we can migrate at the beginning of the function
	// 4. Ensure schema version is set and if not default to 0
	inputVersion := schemaversion.GetSchemaVersion(dash)
	dash["schemaVersion"] = inputVersion

	// If the schema version is older than the minimum version, with migration support,
	// we don't migrate the dashboard.
	if inputVersion < schemaversion.MIN_VERSION {
		return schemaversion.NewMinimumVersionError(inputVersion)
	}

	// 5. Run existing migration pipeline UNCHANGED
	// (All the existing v28, v29, etc. migrators run exactly as before)
	for nextVersion := inputVersion + 1; nextVersion <= targetVersion; nextVersion++ {
		if migration, ok := m.migrations[nextVersion]; ok {
			if err := migration(ctx, dash); err != nil {
				functionName := fmt.Sprintf("V%d", nextVersion)
				return schemaversion.NewMigrationError("migration failed: "+err.Error(), inputVersion, nextVersion, functionName)
			}
			dash["schemaVersion"] = nextVersion
		}
	}

	// 6. Add built-in annotation query after all migrations are complete
	// This matches the frontend DashboardModel constructor behavior
	addBuiltInAnnotationQuery(dash)

	// 7. Clean up the dashboard to match frontend getSaveModel behavior
	// This removes properties that shouldn't be persisted and filters out default values
	cleanupDashboardForSave(dash)

	if schemaversion.GetSchemaVersion(dash) != targetVersion {
		return schemaversion.NewMigrationError("schema version not migrated to target version", inputVersion, targetVersion, "")
	}

	return nil
}

// shouldShowRows determines if row panels will be created (showRows logic)
func shouldShowRows(rows []interface{}) bool {
	for _, rowInterface := range rows {
		row, ok := rowInterface.(map[string]interface{})
		if !ok {
			continue
		}

		collapse := schemaversion.GetBoolValue(row, "collapse")
		showTitle := schemaversion.GetBoolValue(row, "showTitle")
		repeat := schemaversion.GetStringValue(row, "repeat")

		if collapse || showTitle || repeat != "" {
			return true
		}
	}
	return false
}

// applyRowPanelDefaults applies panel defaults to panels within a row based on frontend logic
func applyRowPanelDefaults(row map[string]interface{}, showRows bool) {
	rowPanels, ok := row["panels"].([]interface{})
	if !ok {
		return
	}

	collapse := schemaversion.GetBoolValue(row, "collapse")

	// Frontend: if (rowPanelModel && rowPanel.collapsed) { push(panel) } else { push(new PanelModel(panel)) }
	// Only non-collapsed panels get PanelModel defaults (refId: "A", overrides: [], etc.)
	applyDefaults := !showRows || !collapse

	if !applyDefaults {
		return
	}

	for _, panelInterface := range rowPanels {
		panel, ok := panelInterface.(map[string]interface{})
		if !ok {
			continue
		}
		applyPanelDefaults(panel)
	}
}
