package cluster

import (
	"context"
	"fmt"
	"net/http"

	"github.com/porter-dev/porter/api/server/authz"
	"github.com/porter-dev/porter/api/server/handlers"
	"github.com/porter-dev/porter/api/server/shared"
	"github.com/porter-dev/porter/api/server/shared/apierrors"
	"github.com/porter-dev/porter/api/server/shared/config"
	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/auth/token"
	"github.com/porter-dev/porter/internal/helm"
	"github.com/porter-dev/porter/internal/helm/loader"
	"github.com/porter-dev/porter/internal/kubernetes"
	"github.com/porter-dev/porter/internal/kubernetes/nodes"
	"github.com/porter-dev/porter/internal/models"
	"github.com/porter-dev/porter/internal/telemetry"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
	monitoringNodeLabel = "porter.run/workload-kind=monitoring"
	olderAgentLabel     = "control-plane=controller-manager"
)

type InstallAgentHandler struct {
	handlers.PorterHandlerReadWriter
	authz.KubernetesAgentGetter
}

func NewInstallAgentHandler(
	config *config.Config,
	decoderValidator shared.RequestDecoderValidator,
	writer shared.ResultWriter,
) *InstallAgentHandler {
	return &InstallAgentHandler{
		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
	}
}

func (c *InstallAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx, span := telemetry.NewSpan(r.Context(), "serve-install-agent-handler")
	defer span.End()

	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
	user, _ := ctx.Value(types.UserScope).(*models.User)
	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)

	k8sAgent, err := c.GetAgent(r, cluster, "porter-agent-system")
	if err != nil {
		err = telemetry.Error(ctx, span, err, "failed to get kubernetes agent")
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
		return
	}

	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, "porter-agent-system")
	if err != nil {
		err = telemetry.Error(ctx, span, err, "failed to get helm agent")
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
		return
	}

	err = checkAndDeleteOlderAgent(k8sAgent, helmAgent)
	if err != nil {
		err = telemetry.Error(ctx, span, err, "unable to delete older agent")
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
		return
	}

	chart, err := loader.LoadChartPublic(ctx, c.Config().ServerConf.DefaultAddonHelmRepoURL, "porter-agent", "")
	if err != nil {
		err = telemetry.Error(ctx, span, err, "failed load public porter-agent chart")
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
		return
	}

	// create namespace if not exists
	_, err = helmAgent.K8sAgent.CreateNamespace("porter-agent-system", nil)
	if err != nil {
		err = telemetry.Error(ctx, span, err, "failed to get create porter-agent-system namespace")
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
		return
	}

	// add api token to values
	jwt, err := token.GetTokenForAPI(user.ID, proj.ID)
	if err != nil {
		err = telemetry.Error(ctx, span, err, "failed to get porter-agent api token")
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
		return
	}

	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
	if err != nil {
		err = telemetry.Error(ctx, span, err, "failed to encode porter-agent api token")
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
		return
	}

	nodes, err := nodes.ListNodesByLabels(k8sAgent.Clientset, "porter.run/workload-kind=monitoring")
	hasMonitoringNodes := err == nil && len(nodes) >= 1

	porterAgentValues := map[string]interface{}{
		"agent": map[string]interface{}{
			"porterHost":  c.Config().ServerConf.ServerURL,
			"porterPort":  "443",
			"porterToken": encoded,
			"clusterID":   fmt.Sprintf("%d", cluster.ID),
			"projectID":   fmt.Sprintf("%d", proj.ID),
		},
		"loki": map[string]interface{}{},
	}

	// case on whether a node with porter.run/workload-kind=monitoring exists. If it does, we place loki in that node group.
	if hasMonitoringNodes {
		sharedNS := map[string]interface{}{
			"porter.run/workload-kind": "monitoring",
		}

		sharedTolerations := []map[string]interface{}{
			{
				"key":      "porter.run/workload-kind",
				"operator": "Equal",
				"value":    "monitoring",
				"effect":   "NoSchedule",
			},
		}

		porterAgentValues["loki"] = map[string]interface{}{
			"nodeSelector": sharedNS,
			"tolerations":  sharedTolerations,
		}

		porterAgentValues["nodeSelector"] = sharedNS
		porterAgentValues["tolerations"] = sharedTolerations
	}

	conf := &helm.InstallChartConfig{
		Chart:     chart,
		Name:      "porter-agent",
		Namespace: "porter-agent-system",
		Cluster:   cluster,
		Repo:      c.Repo(),
		Values:    porterAgentValues,
	}

	_, err = helmAgent.InstallChart(context.Background(), conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
	if err != nil {
		err = telemetry.Error(ctx, span, err, "error installing porter-agent")
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
		return
	}

	w.WriteHeader(http.StatusOK)
}

func checkAndDeleteOlderAgent(k8sAgent *kubernetes.Agent, helmAgent *helm.Agent) error {
	namespaceList, err := k8sAgent.Clientset.CoreV1().Namespaces().List(context.Background(), v1.ListOptions{})
	if err != nil {
		return fmt.Errorf("error listing namespaces: %w", err)
	}

	nsExists := false

	for _, namespace := range namespaceList.Items {
		if namespace.Name == "porter-agent-system" {
			nsExists = true
			break
		}
	}

	if !nsExists {
		return nil
	}

	// detect if the `porter-agent` release is installed
	helmRelease, err := helmAgent.GetRelease(context.Background(), "porter-agent", 0, false)

	if err != nil || helmRelease == nil {
		return nil
	}

	_, err = helmAgent.UninstallChart(context.Background(), "porter-agent")

	if err != nil {
		return err
	}

	return nil
}
