package deployer

import (
	"context"
	"errors"
	"fmt"
	"os"
	"time"

	"oras.land/oras-go/v2"
	"oras.land/oras-go/v2/content/file"
	"oras.land/oras-go/v2/registry/remote"

	"github.com/kubeasy-dev/kubeasy-cli/internal/kube"
	"github.com/kubeasy-dev/kubeasy-cli/internal/logger"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/restmapper"
)

// DeployChallenge pulls the challenge OCI artifact and applies manifests to the cluster.
func DeployChallenge(ctx context.Context, clientset *kubernetes.Clientset, dynamicClient dynamic.Interface, slug string) error {
	logger.Info("Deploying challenge '%s' from OCI registry...", slug)

	// Create temporary directory for extracted artifacts
	tmpDir, err := os.MkdirTemp("", "kubeasy-challenge-*")
	if err != nil {
		return fmt.Errorf("failed to create temp directory: %w", err)
	}
	defer func() { _ = os.RemoveAll(tmpDir) }()

	// Pull OCI artifact
	ref := fmt.Sprintf("%s/%s:latest", ChallengesOCIRegistry, slug)
	logger.Debug("Pulling OCI artifact: %s", ref)

	if err := pullOCIArtifact(ctx, ref, tmpDir); err != nil {
		return fmt.Errorf("failed to pull challenge artifact from %s: %w", ref, err)
	}

	// Build REST mapper from API discovery
	groups, err := restmapper.GetAPIGroupResources(clientset.Discovery())
	if err != nil {
		return fmt.Errorf("failed to discover API resources: %w", err)
	}
	mapper := restmapper.NewDiscoveryRESTMapper(groups)

	// Find and apply YAML files from manifests/ and policies/
	if err := applyManifestDirs(ctx, tmpDir, slug, mapper, dynamicClient); err != nil {
		return err
	}

	// Wait for Deployments and StatefulSets to be ready.
	// Use a separate context so the readiness wait never exhausts the caller's context.
	// Fix-type challenges deploy intentionally broken resources — a deadline exceeded error
	// is expected and ignored. Other errors (context cancellation, API failures) propagate.
	logger.Info("Waiting for challenge resources to be ready...")
	waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer waitCancel()
	if err := WaitForChallengeReady(waitCtx, clientset, slug); err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			logger.Info("Challenge '%s' deployed (some resources are not ready yet).", slug)
		} else {
			return fmt.Errorf("challenge resources failed to become ready: %w", err)
		}
	} else {
		logger.Info("Challenge '%s' deployed successfully.", slug)
	}

	return nil
}

// pullOCIArtifact pulls an OCI artifact from the registry to the target directory.
func pullOCIArtifact(ctx context.Context, ref string, targetDir string) error {
	repo, err := remote.NewRepository(ref)
	if err != nil {
		return fmt.Errorf("failed to create OCI repository reference: %w", err)
	}

	store, err := file.New(targetDir)
	if err != nil {
		return fmt.Errorf("failed to create file store: %w", err)
	}
	defer func() { _ = store.Close() }()

	_, err = oras.Copy(ctx, repo, repo.Reference.Reference, store, "", oras.DefaultCopyOptions)
	if err != nil {
		return fmt.Errorf("failed to pull OCI artifact: %w", err)
	}

	return nil
}

// WaitForChallengeReady waits for all Deployments and StatefulSets in the namespace to be ready.
func WaitForChallengeReady(ctx context.Context, clientset kubernetes.Interface, namespace string) error {
	// List Deployments
	deployments, err := clientset.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{})
	if err != nil {
		return fmt.Errorf("failed to list deployments: %w", err)
	}

	if len(deployments.Items) > 0 {
		names := make([]string, 0, len(deployments.Items))
		for _, d := range deployments.Items {
			names = append(names, d.Name)
		}
		if err := kube.WaitForDeploymentsReady(ctx, clientset, namespace, names); err != nil {
			return err
		}
	}

	// List StatefulSets
	statefulsets, err := clientset.AppsV1().StatefulSets(namespace).List(ctx, metav1.ListOptions{})
	if err != nil {
		return fmt.Errorf("failed to list statefulsets: %w", err)
	}

	if len(statefulsets.Items) > 0 {
		names := make([]string, 0, len(statefulsets.Items))
		for _, s := range statefulsets.Items {
			names = append(names, s.Name)
		}
		if err := kube.WaitForStatefulSetsReady(ctx, clientset, namespace, names); err != nil {
			return err
		}
	}

	return nil
}
