package app

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/grafana/authlib/types"
	"github.com/grafana/grafana-app-sdk/logging"
	"github.com/grafana/grafana-app-sdk/resource"
	advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
	"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
	"github.com/grafana/grafana/apps/advisor/pkg/app/metrics"
	"github.com/grafana/grafana/pkg/services/contexthandler"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
)

var retryAnnotationPollingInterval = 1 * time.Second

func getCheck(obj resource.Object, checkMap map[string]checks.Check) (checks.Check, error) {
	labels := obj.GetLabels()
	objTypeLabel, ok := labels[checks.TypeLabel]
	if !ok {
		return nil, errors.New("missing check type as label")
	}
	c, ok := checkMap[objTypeLabel]
	if !ok {
		supportedTypes := ""
		for k := range checkMap {
			supportedTypes += k + ", "
		}
		return nil, fmt.Errorf("unknown check type %s. Supported types are: %s", objTypeLabel, supportedTypes)
	}

	return c, nil
}

func processCheck(ctx context.Context, log logging.Logger, client resource.Client, typesClient resource.Client, obj resource.Object, check checks.Check) error {
	status := checks.GetStatusAnnotation(obj)
	if status != "" {
		// Check already processed
		return nil
	}
	c, ok := obj.(*advisorv0alpha1.Check)
	if !ok {
		return fmt.Errorf("invalid object type")
	}
	// Get the items to check
	err := check.Init(ctx)
	if err != nil {
		return fmt.Errorf("error initializing check: %w", err)
	}
	items, err := check.Items(ctx)
	if err != nil {
		setErr := checks.SetStatusAnnotation(ctx, client, obj, checks.StatusAnnotationError)
		if setErr != nil {
			return setErr
		}
		return fmt.Errorf("error listing items for check: %w", err)
	}
	// Get the check type
	var checkType resource.Object
	checkType, err = typesClient.Get(ctx, resource.Identifier{
		Namespace: obj.GetNamespace(),
		Name:      check.ID(),
	})
	if err != nil {
		return err
	}
	// Run the steps
	steps, err := filterSteps(checkType, check.Steps())
	if err != nil {
		return err
	}
	failures, err := runStepsInParallel(ctx, log, &c.Spec, steps, items)
	if err != nil {
		setErr := checks.SetStatusAnnotation(ctx, client, obj, checks.StatusAnnotationError)
		if setErr != nil {
			return setErr
		}
		return fmt.Errorf("error running steps: %w", err)
	}
	// Wait for the item to be persisted before patching the object
	err = waitForItem(ctx, log, client, obj)
	if err != nil {
		return err
	}
	report := &advisorv0alpha1.CheckReport{
		Failures: failures,
		Count:    int64(len(items)),
	}
	c.Status.Report = *report
	err = checks.SetStatus(ctx, client, obj, c.Status)
	if err != nil {
		return err
	}
	// Set the status annotation to processed and annotate the steps ignored
	annotations := checks.AddAnnotations(ctx, obj, map[string]string{
		checks.StatusAnnotation:          checks.StatusAnnotationProcessed,
		checks.IgnoreStepsAnnotationList: checkType.GetAnnotations()[checks.IgnoreStepsAnnotationList],
	})
	return checks.SetAnnotations(ctx, client, obj, annotations)
}

func processCheckRetry(ctx context.Context, log logging.Logger, client resource.Client, typesClient resource.Client, obj resource.Object, check checks.Check) error {
	status := checks.GetStatusAnnotation(obj)
	if status == "" || status == checks.StatusAnnotationError {
		// Check not processed yet or errored
		log.Debug("Check not processed yet or errored, skipping retry", "check", obj.GetName(), "status", status)
		return nil
	}
	// Get the item to retry from the annotation
	itemToRetry := checks.GetRetryAnnotation(obj)
	if itemToRetry == "" {
		// No item to retry, nothing to do
		log.Debug("No item to retry, skipping retry", "check", obj.GetName())
		return nil
	} else {
		log.Debug("Item to retry found", "check", obj.GetName(), "item", itemToRetry)
	}
	c, ok := obj.(*advisorv0alpha1.Check)
	if !ok {
		return fmt.Errorf("invalid object type")
	}
	// Get the items to check
	err := check.Init(ctx)
	if err != nil {
		return fmt.Errorf("error initializing check: %w", err)
	}
	failures := []advisorv0alpha1.CheckReportFailure{}
	item, err := check.Item(ctx, itemToRetry)
	if err != nil {
		setErr := checks.SetStatusAnnotation(ctx, client, obj, checks.StatusAnnotationError)
		if setErr != nil {
			return setErr
		}
		return fmt.Errorf("error getting item for check: %w", err)
	}
	if item != nil {
		// Get the check type
		var checkType resource.Object
		checkType, err = typesClient.Get(ctx, resource.Identifier{
			Namespace: obj.GetNamespace(),
			Name:      check.ID(),
		})
		if err != nil {
			return err
		}
		// Run the steps
		steps, err := filterSteps(checkType, check.Steps())
		if err != nil {
			return err
		}
		failures, err = runStepsInParallel(ctx, log, &c.Spec, steps, []any{item})
		if err != nil {
			setErr := checks.SetStatusAnnotation(ctx, client, obj, checks.StatusAnnotationError)
			if setErr != nil {
				return setErr
			}
			return fmt.Errorf("error running steps: %w", err)
		}
	}
	// Pull this item's failures and replace them with the retry result
	currentFailures := slices.DeleteFunc(slices.Clone(c.Status.Report.Failures), func(f advisorv0alpha1.CheckReportFailure) bool {
		return f.ItemID == itemToRetry
	})
	c.Status.Report.Failures = append(currentFailures, failures...)
	// Wait for the retry annotation to be persisted before patching the object
	err = waitForRetryAnnotation(ctx, log, client, obj, itemToRetry)
	if err != nil {
		return err
	}
	// Set the status
	err = checks.SetStatus(ctx, client, obj, c.Status)
	log.Debug("Status set", "check", obj.GetName(), "status.count", c.Status.Report.Count)
	if err != nil {
		return err
	}
	// Delete the retry annotation to mark the check as processed
	annotations := checks.DeleteAnnotations(ctx, obj, []string{checks.RetryAnnotation})
	err = checks.SetAnnotations(ctx, client, obj, annotations)
	log.Debug("Annotations set", "check", obj.GetName(), "annotations", annotations)

	return err
}

func runStepsInParallel(ctx context.Context, log logging.Logger, spec *advisorv0alpha1.CheckSpec, steps []checks.Step, items []any) ([]advisorv0alpha1.CheckReportFailure, error) {
	reportFailures := []advisorv0alpha1.CheckReportFailure{}
	var internalErr error
	var wg sync.WaitGroup
	var mu sync.Mutex
	// Avoid too many concurrent requests
	limit := make(chan struct{}, 10)

	for _, step := range steps {
		for _, item := range items {
			wg.Add(1)
			limit <- struct{}{}
			go func(step checks.Step, item any) {
				defer wg.Done()
				defer func() { <-limit }()
				var stepErr []advisorv0alpha1.CheckReportFailure
				var err error
				func() {
					defer func() {
						if r := recover(); r != nil {
							log.Error("panic recovered in step", "step", step.ID(), "error", r, "item", item)
							metrics.StepPanicsTotal.WithLabelValues(step.ID()).Inc()
						}
					}()
					logger := log.With("step", step.ID())
					// Create a copy of the context with a cloned HTTP request to prevent
					// concurrent modifications to the same header map
					safeCtx := contexthandler.CopyWithReqContext(ctx)
					stepErr, err = step.Run(safeCtx, logger, spec, item)
				}()
				mu.Lock()
				defer mu.Unlock()
				if err != nil {
					internalErr = fmt.Errorf("error running step %s: %w", step.ID(), err)
					return
				}
				if len(stepErr) > 0 {
					reportFailures = append(reportFailures, stepErr...)
				}
			}(step, item)
		}
	}
	wg.Wait()
	return reportFailures, internalErr
}

func filterSteps(checkType resource.Object, steps []checks.Step) ([]checks.Step, error) {
	ignoreStepsList := checkType.GetAnnotations()[checks.IgnoreStepsAnnotationList]
	if ignoreStepsList != "" {
		filteredSteps := []checks.Step{}
		ignoreStepsList := strings.Split(ignoreStepsList, ",")
		for _, step := range steps {
			if !slices.Contains(ignoreStepsList, step.ID()) {
				filteredSteps = append(filteredSteps, step)
			}
		}
		return filteredSteps, nil
	}
	return steps, nil
}

// retryAnnotationChanged compares the retry annotation between old and new objects
func retryAnnotationChanged(oldObj, newObj resource.Object) bool {
	if oldObj == nil || newObj == nil {
		return true // If either is nil, consider it changed
	}

	// Compare annotations
	oldAnnotations := oldObj.GetAnnotations()
	newAnnotations := newObj.GetAnnotations()
	return newAnnotations[checks.RetryAnnotation] != "" &&
		oldAnnotations[checks.RetryAnnotation] != newAnnotations[checks.RetryAnnotation]
}

func waitForItem(ctx context.Context, log logging.Logger, client resource.Client, obj resource.Object) error {
	_, err := client.Get(ctx, resource.Identifier{
		Namespace: obj.GetNamespace(),
		Name:      obj.GetName(),
	})
	retries := 0
	for err != nil && k8serrors.IsNotFound(err) && retries < 5 {
		log.Debug("Waiting for item to be persisted", "check", obj.GetName(), "retries", retries)
		time.Sleep(retryAnnotationPollingInterval)
		retries++
		_, err = client.Get(ctx, resource.Identifier{
			Namespace: obj.GetNamespace(),
			Name:      obj.GetName(),
		})
	}
	return err
}

// waitForRetryAnnotation waits for the retry annotation to match the item to retry
func waitForRetryAnnotation(ctx context.Context, log logging.Logger, client resource.Client, obj resource.Object, itemToRetry string) error {
	currentObj, err := client.Get(ctx, resource.Identifier{
		Namespace: obj.GetNamespace(),
		Name:      obj.GetName(),
	})
	if err != nil {
		return err
	}
	retries := 0
	currentRetryAnnotation := checks.GetRetryAnnotation(currentObj)
	for currentRetryAnnotation != itemToRetry {
		log.Debug("Waiting for retry annotation to be persisted", "check", obj.GetName(), "item", itemToRetry, "currentRetryAnnotation", currentRetryAnnotation)
		time.Sleep(retryAnnotationPollingInterval)
		retries++
		if retries > 5 {
			return fmt.Errorf("timeout waiting for retry annotation to be persisted")
		}
		currentObj, err = client.Get(ctx, resource.Identifier{
			Namespace: obj.GetNamespace(),
			Name:      obj.GetName(),
		})
		if err != nil {
			return err
		}
		currentRetryAnnotation = checks.GetRetryAnnotation(currentObj)
	}
	log.Debug("Retry annotation persisted", "check", obj.GetName(), "item", itemToRetry)
	return nil
}

// getOrgIDFromNamespace extracts the org ID from a namespace using the standard authlib parser.
func getOrgIDFromNamespace(namespace string) (int64, error) {
	info, err := types.ParseNamespace(namespace)
	if err != nil {
		return 0, fmt.Errorf("failed to parse namespace %s: %w", namespace, err)
	}
	return info.OrgID, nil
}
