// Package operator implements the core container-mom operator functionality
package operator

import (
	"context"
	"fmt"
	"reflect"
	"time"

	v1alpha1 "github.com/pfeifferj/container-mom-go/pkg/apis/containermom/v1alpha1"
	"github.com/pfeifferj/container-mom-go/pkg/operator/mongodb"
	"github.com/pfeifferj/container-mom-go/pkg/operator/mongodb/api"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/util/workqueue"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// DeploymentController manages the lifecycle of ContainerMomDeployments
// nolint:staticcheck // TODO: Migrate to newer workqueue API when available
type DeploymentController struct {
	mongodb     mongodb.Interface
	reconciler  *DeploymentReconciler
	queue       workqueue.RateLimitingInterface
	lastSyncMap map[string]time.Time // Track last sync time per deployment
	client      client.Client
}

// NewDeploymentController creates a new DeploymentController instance
// nolint:staticcheck // TODO: Migrate to newer workqueue API when available
func NewDeploymentController(mongodb mongodb.Interface, reconciler *DeploymentReconciler, client client.Client) *DeploymentController {
	return &DeploymentController{
		mongodb:     mongodb,
		reconciler:  reconciler,
		queue:       workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
		lastSyncMap: make(map[string]time.Time),
		client:      client,
	}
}

// Start begins watching and reconciling deployments
func (c *DeploymentController) Start(ctx context.Context) error {
	ctrl.Log.Info("starting MongoDB watch")

	// Initial sync to create CRs for existing deployments
	if err := c.fullSync(ctx); err != nil {
		return fmt.Errorf("initial sync failed: %w", err)
	}

	// Start the worker
	go c.worker(ctx)

	// Start periodic full sync
	go c.periodicFullSync(ctx)

	ctrl.Log.Info("deployment controller started successfully")
	<-ctx.Done()
	return nil
}

func (c *DeploymentController) worker(ctx context.Context) {
	for {
		obj, shutdown := c.queue.Get()
		if shutdown {
			return
		}

		func() {
			defer c.queue.Done(obj)
			deploymentID, ok := obj.(string)
			if !ok {
				ctrl.Log.Error(nil, "failed to process non-string deployment ID", "object", obj)
				c.queue.Forget(obj)
				return
			}

			if err := c.reconcileDeployment(ctx, deploymentID); err != nil {
				if c.queue.NumRequeues(obj) < 5 {
					c.queue.AddRateLimited(obj)
					return
				}
				c.queue.Forget(obj)
			}
		}()
	}
}

func (c *DeploymentController) periodicFullSync(ctx context.Context) {
	ticker := time.NewTicker(5 * time.Minute)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := c.fullSync(ctx); err != nil {
				ctrl.Log.Error(err, "failed to perform full sync")
			}
		}
	}
}

func (c *DeploymentController) fullSync(ctx context.Context) error {
	ctrl.Log.Info("performing full sync")
	// Get all deployments from MongoDB
	deployments, err := c.mongodb.ListDeployments(ctx)
	if err != nil {
		ctrl.Log.Error(err, "failed to fetch deployments from MongoDB")
		return fmt.Errorf("failed to fetch deployments: %w", err)
	}

	ctrl.Log.Info("found deployments in MongoDB", "count", len(deployments))
	for _, d := range deployments {
		ctrl.Log.Info("found deployment",
			"name", d.Name,
			"namespace", d.Namespace,
			"gitUrl", d.GitURL,
		)
	}

	// Track deployments we've seen
	seenDeployments := make(map[string]map[string]bool)

	// Process each deployment
	for _, deployment := range deployments {
		if seenDeployments[deployment.Namespace] == nil {
			seenDeployments[deployment.Namespace] = make(map[string]bool)
		}
		seenDeployments[deployment.Namespace][deployment.Name] = true

		// Convert MongoDB deployment to CR
		cmDeployment := c.convertToK8sDeployment(&deployment)

		// Check if CR already exists
		existing := &v1alpha1.ContainerMomDeployment{}
		err := c.client.Get(ctx, client.ObjectKey{
			Name:      deployment.Name,
			Namespace: deployment.Namespace,
		}, existing)

		if err != nil {
			if client.IgnoreNotFound(err) != nil {
				ctrl.Log.Error(err, "failed to check existing deployment", "deployment", deployment.Name)
				continue
			}
			// Create new CR
			if err := c.client.Create(ctx, cmDeployment); err != nil {
				ctrl.Log.Error(err, "failed to create deployment CR", "deployment", deployment.Name)
				continue
			}
			ctrl.Log.Info("created new deployment CR", "deployment", deployment.Name)
		} else {
			// Update existing CR if needed
			if !reflect.DeepEqual(existing.Spec, cmDeployment.Spec) {
				existing.Spec = cmDeployment.Spec
				if err := c.client.Update(ctx, existing); err != nil {
					ctrl.Log.Error(err, "failed to update deployment CR", "deployment", deployment.Name)
					continue
				}
				ctrl.Log.Info("updated deployment CR", "deployment", deployment.Name)
			}
		}
	}

	// Clean up CRs that no longer exist in MongoDB
	var existingList v1alpha1.ContainerMomDeploymentList
	if err := c.client.List(ctx, &existingList, client.InNamespace("container-mom-system")); err != nil {
		ctrl.Log.Error(err, "failed to list existing CRs")
		return fmt.Errorf("failed to list existing CRs: %w", err)
	}

	for _, existing := range existingList.Items {
		if !seenDeployments[existing.Namespace][existing.Name] {
			// Double check the deployment doesn't exist in MongoDB
			_, err := c.mongodb.GetDeployment(ctx, existing.Name)
			if err != nil && err.Error() == "deployment not found" {
				if err := c.client.Delete(ctx, &existing); err != nil {
					ctrl.Log.Error(err, "failed to delete CR", "deployment", existing.Name)
					continue
				}
				ctrl.Log.Info("deleted CR for non-existent deployment", "deployment", existing.Name)
			}
		}
	}

	return nil
}

func (c *DeploymentController) reconcileDeployment(ctx context.Context, deploymentID string) error {
	// Get deployment from MongoDB interface
	deployment, err := c.mongodb.GetDeployment(ctx, deploymentID)
	if err != nil {
		// If deployment not found, handle deletion case
		if err.Error() == "deployment not found" {
			// Check if k8s deployment exists before trying to delete it
			k8sDeployment := &v1alpha1.ContainerMomDeployment{}
			err := c.client.Get(ctx, client.ObjectKey{
				Name:      deploymentID,
				Namespace: "default", // Use default namespace for now
			}, k8sDeployment)
			if err == nil {
				// K8s deployment exists, delete it
				return c.reconciler.deleteDeployment(ctx, deploymentID)
			}
			// If k8s deployment doesn't exist, that's fine - nothing to delete
			return nil
		}
		return fmt.Errorf("failed to get deployment: %w", err)
	}

	// Convert to ContainerMomDeployment
	cmDeployment := c.convertToK8sDeployment(deployment)

	// Apply the deployment
	return c.reconciler.reconcileDeployment(ctx, cmDeployment)
}

func (c *DeploymentController) convertToK8sDeployment(mongoDeployment *api.MongoDeployment) *v1alpha1.ContainerMomDeployment {
	ctrl.Log.Info("converting MongoDB deployment to CR",
		"name", mongoDeployment.Name,
		"namespace", mongoDeployment.Namespace,
	)

	return &v1alpha1.ContainerMomDeployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      mongoDeployment.Name,
			Namespace: mongoDeployment.Namespace,
		},
		Spec: v1alpha1.ContainerMomDeploymentSpec{
			Source: v1alpha1.SourceSpec{
				GitURL: mongoDeployment.GitURL,
				Image:  mongoDeployment.ContainerImage,
			},
			Clusters: mongoDeployment.Regions,
			Resources: v1alpha1.ResourceRequirements{
				CPU:    mongoDeployment.Resources.CPU,
				Memory: mongoDeployment.Resources.Memory,
			},
			Domain: v1alpha1.DomainSpec{
				Host:        mongoDeployment.Domain,
				UseWildcard: mongoDeployment.UseWildcard,
				Path:        mongoDeployment.Subdomain,
			},
			Env: convertEnvVars(mongoDeployment.Env),
		},
	}
}

func convertEnvVars(envVars []v1alpha1.EnvVar) []v1alpha1.EnvVar {
	// Since both types are the same, we can just return the input
	return envVars
}

// EnqueueDeployment adds a deployment to the reconciliation queue
func (c *DeploymentController) EnqueueDeployment(deploymentID string) {
	c.queue.Add(deploymentID)
}
