package conversion

import (
	"errors"
	"fmt"
	"math"
	"strings"

	"k8s.io/apimachinery/pkg/conversion"

	"github.com/grafana/grafana-app-sdk/logging"
	dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
	dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
	dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
	dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
	"github.com/grafana/grafana/apps/dashboard/pkg/migration"
	"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)

func getLogger() logging.Logger {
	return logging.DefaultLogger.With("logger", "dashboard.conversion")
}

// getErroredSchemaVersionFunc determines the schema version function that errored
func getErroredSchemaVersionFunc(err error) string {
	var migrationErr *schemaversion.MigrationError
	if errors.As(err, &migrationErr) {
		return migrationErr.GetFunctionName()
	}
	return ""
}

// getErroredConversionFunc determines the conversion function that errored
func getErroredConversionFunc(err error) string {
	var conversionErr *ConversionError
	if errors.As(err, &conversionErr) {
		return conversionErr.GetFunctionName()
	}

	var migrationErr *schemaversion.MigrationError
	if errors.As(err, &migrationErr) {
		return migrationErr.GetFunctionName()
	}

	var dataLossErr *ConversionDataLossError
	if errors.As(err, &dataLossErr) {
		return dataLossErr.GetFunctionName()
	}

	return ""
}

// convertAPIVersionToFuncName converts API version to function name format
func convertAPIVersionToFuncName(apiVersion string) string {
	// Convert dashboard.grafana.app/v0alpha1 to v0alpha1
	if idx := strings.LastIndex(apiVersion, "/"); idx != -1 {
		apiVersion = apiVersion[idx+1:]
	}

	// Map API versions to function name format
	switch apiVersion {
	case "v0alpha1":
		return "V0"
	case "v1beta1":
		return "V1"
	case "v2alpha1":
		return "V2alpha1"
	case "v2beta1":
		return "V2beta1"
	default:
		return apiVersion
	}
}

// withConversionMetrics wraps a conversion function with metrics and logging for the overall conversion process
// It optionally runs a data loss check function after successful conversion
func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversionFunc func(a, b interface{}, scope conversion.Scope) error) func(a, b interface{}, scope conversion.Scope) error {
	return func(a, b interface{}, scope conversion.Scope) error {
		// Extract dashboard UID and schema version from source
		var dashboardUID string
		var sourceSchemaVersion interface{}
		var targetSchemaVersion interface{}

		// Try to extract UID and schema version from source dashboard
		// Only track schema versions for v0/v1 dashboards (v2+ info is redundant with API version)
		switch source := a.(type) {
		case *dashv0.Dashboard:
			dashboardUID = source.Name
			if source.Spec.Object != nil {
				sourceSchemaVersion = schemaversion.GetSchemaVersion(source.Spec.Object)
			}
		case *dashv1.Dashboard:
			dashboardUID = source.Name
			if source.Spec.Object != nil {
				sourceSchemaVersion = schemaversion.GetSchemaVersion(source.Spec.Object)
			}
		case *dashv2alpha1.Dashboard:
			dashboardUID = source.Name
			// Don't track schema version for v2+ (redundant with API version)
		case *dashv2beta1.Dashboard:
			dashboardUID = source.Name
			// Don't track schema version for v2+ (redundant with API version)
		}

		// Determine target schema version based on target type
		// Only for v0/v1 dashboards
		switch b.(type) {
		case *dashv0.Dashboard:
			if sourceSchemaVersion != nil {
				targetSchemaVersion = sourceSchemaVersion // V0 keeps source schema version
			}
		case *dashv1.Dashboard:
			if sourceSchemaVersion != nil {
				targetSchemaVersion = schemaversion.LATEST_VERSION // V1 migrates to latest
			}
		case *dashv2alpha1.Dashboard:
			// Don't track schema version for v2+ (redundant with API version)
		case *dashv2beta1.Dashboard:
			// Don't track schema version for v2+ (redundant with API version)
		}

		// Execute the actual conversion
		err := conversionFunc(a, b, scope)

		// If conversion succeeded, run data loss check
		if err == nil {
			err = checkConversionDataLoss(sourceVersionAPI, targetVersionAPI, a, b)
		}

		// Report conversion-level metrics and logs
		if err != nil {
			// Classify error type for metrics
			errorType := "conversion_error"
			var migrationErr *schemaversion.MigrationError
			var minVersionErr *schemaversion.MinimumVersionError
			var dataLossErr *ConversionDataLossError
			if errors.As(err, &migrationErr) {
				errorType = "schema_version_migration_error"
			} else if errors.As(err, &minVersionErr) {
				errorType = "schema_minimum_version_error"
			} else if errors.As(err, &dataLossErr) {
				errorType = "conversion_data_loss_error"
			}

			// Record failure metrics
			sourceSchemaStr := ""
			targetSchemaStr := ""
			if sourceSchemaVersion != nil {
				sourceSchemaStr = fmt.Sprintf("%v", sourceSchemaVersion)
			}
			if targetSchemaVersion != nil {
				targetSchemaStr = fmt.Sprintf("%v", targetSchemaVersion)
			}

			migration.MDashboardConversionFailureTotal.WithLabelValues(
				sourceVersionAPI,
				targetVersionAPI,
				sourceSchemaStr,
				targetSchemaStr,
				errorType,
			).Inc()

			// Log failure - use warning for schema_minimum_version_error, error for others
			// Build base log fields
			logFields := []interface{}{
				"sourceVersionAPI", sourceVersionAPI,
				"targetVersionAPI", targetVersionAPI,
				"erroredConversionFunc", getErroredConversionFunc(err),
				"dashboardUID", dashboardUID,
			}

			// Add schema version fields only if we have them (v0/v1 dashboards)
			if sourceSchemaVersion != nil && targetSchemaVersion != nil {
				logFields = append(logFields,
					"sourceSchemaVersion", sourceSchemaVersion,
					"targetSchemaVersion", targetSchemaVersion,
					"erroredSchemaVersionFunc", getErroredSchemaVersionFunc(err),
				)
			}

			// Add data loss specific fields if this is a data loss error
			if errorType == "conversion_data_loss_error" {
				sourceStats := collectDashboardStats(a)
				targetStats := collectDashboardStats(b)
				// We consider losing data when the target is less than the source
				logFields = append(logFields,
					"panelsLost", math.Max(0, float64(sourceStats.panelCount-targetStats.panelCount)),
					"queriesLost", math.Max(0, float64(sourceStats.queryCount-targetStats.queryCount)),
					"annotationsLost", math.Max(0, float64(sourceStats.annotationCount-targetStats.annotationCount)),
					"linksLost", math.Max(0, float64(sourceStats.linkCount-targetStats.linkCount)),
					"variablesLost", math.Max(0, float64(sourceStats.variableCount-targetStats.variableCount)),
				)
			}

			// Add remaining fields
			logFields = append(logFields,
				"errorType", errorType,
				"error", err,
			)

			if errorType == "schema_minimum_version_error" {
				getLogger().Warn("Dashboard conversion failed", logFields...)
			} else {
				getLogger().Error("Dashboard conversion failed", logFields...)
			}
		} else {
			// Record success metrics
			sourceSchemaStr := ""
			targetSchemaStr := ""
			if sourceSchemaVersion != nil {
				sourceSchemaStr = fmt.Sprintf("%v", sourceSchemaVersion)
			}
			if targetSchemaVersion != nil {
				targetSchemaStr = fmt.Sprintf("%v", targetSchemaVersion)
			}

			migration.MDashboardConversionSuccessTotal.WithLabelValues(
				sourceVersionAPI,
				targetVersionAPI,
				sourceSchemaStr,
				targetSchemaStr,
			).Inc()

			// Log success (debug level to avoid spam)
			// Build base log fields for success
			successLogFields := []interface{}{
				"sourceVersionAPI", sourceVersionAPI,
				"targetVersionAPI", targetVersionAPI,
				"dashboardUID", dashboardUID,
			}

			// Add schema version fields only if we have them (v0/v1 dashboards)
			if sourceSchemaVersion != nil && targetSchemaVersion != nil {
				successLogFields = append(successLogFields,
					"sourceSchemaVersion", sourceSchemaVersion,
					"targetSchemaVersion", targetSchemaVersion,
				)
			}

			getLogger().Debug("Dashboard conversion succeeded", successLogFields...)
		}

		return nil
	}
}
