// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package wsmanager

import (
	"fmt"
	"path/filepath"
	"time"

	wsdaemon "github.com/gitpod-io/gitpod/installer/pkg/components/ws-daemon"
	"sigs.k8s.io/yaml"

	"github.com/gitpod-io/gitpod/common-go/grpc"
	"github.com/gitpod-io/gitpod/common-go/util"
	storageconfig "github.com/gitpod-io/gitpod/content-service/api/config"
	"github.com/gitpod-io/gitpod/installer/pkg/common"
	configv1 "github.com/gitpod-io/gitpod/installer/pkg/config/v1"
	"github.com/gitpod-io/gitpod/installer/pkg/config/v1/experimental"
	"github.com/gitpod-io/gitpod/ws-manager/api/config"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
)

func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
	cfgTpls := ctx.Config.Workspace.Templates
	if cfgTpls == nil {
		cfgTpls = &configv1.WorkspaceTemplates{}
	}
	templatesCfg, tpls, err := buildWorkspaceTemplates(ctx, cfgTpls, "")
	if err != nil {
		return nil, err
	}

	quantityString := func(idx corev1.ResourceList, key corev1.ResourceName) string {
		q, ok := idx[key]
		if !ok {
			return ""
		}
		return (&q).String()
	}

	timeoutAfterClose := util.Duration(2 * time.Minute)
	if ctx.Config.Workspace.TimeoutAfterClose != nil {
		timeoutAfterClose = *ctx.Config.Workspace.TimeoutAfterClose
	}

	var customCASecret string
	if ctx.Config.CustomCACert != nil {
		customCASecret = ctx.Config.CustomCACert.Name
	}

	classes := map[string]*config.WorkspaceClass{
		config.DefaultWorkspaceClass: {
			Name: config.DefaultWorkspaceClass,
			Container: config.ContainerConfiguration{
				Requests: &config.ResourceRequestConfiguration{
					CPU:              quantityString(ctx.Config.Workspace.Resources.Requests, corev1.ResourceCPU),
					Memory:           quantityString(ctx.Config.Workspace.Resources.Requests, corev1.ResourceMemory),
					EphemeralStorage: quantityString(ctx.Config.Workspace.Resources.Requests, corev1.ResourceEphemeralStorage),
				},
				Limits: &config.ResourceLimitConfiguration{
					CPU: &config.CpuResourceLimit{
						MinLimit:   quantityString(ctx.Config.Workspace.Resources.Limits, corev1.ResourceCPU),
						BurstLimit: quantityString(ctx.Config.Workspace.Resources.Limits, corev1.ResourceCPU),
					},
					Memory:           quantityString(ctx.Config.Workspace.Resources.Limits, corev1.ResourceMemory),
					EphemeralStorage: quantityString(ctx.Config.Workspace.Resources.Limits, corev1.ResourceEphemeralStorage),
					Storage:          quantityString(ctx.Config.Workspace.Resources.Limits, corev1.ResourceStorage),
				},
			},
			Templates: templatesCfg,
			PrebuildPVC: config.PVCConfiguration{
				Size:          ctx.Config.Workspace.PrebuildPVC.Size,
				StorageClass:  ctx.Config.Workspace.PrebuildPVC.StorageClass,
				SnapshotClass: ctx.Config.Workspace.PrebuildPVC.SnapshotClass,
			},
			PVC: config.PVCConfiguration{
				Size:          ctx.Config.Workspace.PVC.Size,
				StorageClass:  ctx.Config.Workspace.PVC.StorageClass,
				SnapshotClass: ctx.Config.Workspace.PVC.SnapshotClass,
			},
		},
	}

	installationShortNameSuffix := ""
	if ctx.Config.Metadata.InstallationShortname != "" && ctx.Config.Metadata.InstallationShortname != configv1.InstallationShortNameOldDefault {
		installationShortNameSuffix = "-" + ctx.Config.Metadata.InstallationShortname
	}

	var schedulerName string
	gitpodHostURL := "https://" + ctx.Config.Domain
	workspaceClusterHost := fmt.Sprintf("ws%s.%s", installationShortNameSuffix, ctx.Config.Domain)
	workspaceURLTemplate := fmt.Sprintf("https://{{ .Prefix }}.ws%s.%s", installationShortNameSuffix, ctx.Config.Domain)
	workspacePortURLTemplate := fmt.Sprintf("https://{{ .WorkspacePort }}-{{ .Prefix }}.ws%s.%s", installationShortNameSuffix, ctx.Config.Domain)

	rateLimits := map[string]grpc.RateLimit{}

	err = ctx.WithExperimental(func(ucfg *experimental.Config) error {
		if ucfg.Workspace == nil {
			return nil
		}
		for k, c := range ucfg.Workspace.WorkspaceClasses {
			tplsCfg, ctpls, err := buildWorkspaceTemplates(ctx, &configv1.WorkspaceTemplates{
				Default:    c.Templates.Default,
				Prebuild:   c.Templates.Prebuild,
				ImageBuild: c.Templates.ImageBuild,
				Regular:    c.Templates.Regular,
			}, k)
			if err != nil {
				return err
			}
			classes[k] = &config.WorkspaceClass{
				Name: c.Name,
				Container: config.ContainerConfiguration{
					Requests: &config.ResourceRequestConfiguration{
						CPU:              quantityString(c.Resources.Requests, corev1.ResourceCPU),
						Memory:           quantityString(c.Resources.Requests, corev1.ResourceMemory),
						EphemeralStorage: quantityString(c.Resources.Requests, corev1.ResourceEphemeralStorage),
					},
					Limits: &config.ResourceLimitConfiguration{
						CPU: &config.CpuResourceLimit{
							MinLimit:   c.Resources.Limits.Cpu.MinLimit,
							BurstLimit: c.Resources.Limits.Cpu.BurstLimit,
						},
						Memory:           c.Resources.Limits.Memory,
						EphemeralStorage: c.Resources.Limits.EphemeralStorage,
						Storage:          c.Resources.Limits.Storage,
					},
				},
				Templates:   tplsCfg,
				PrebuildPVC: config.PVCConfiguration(c.PrebuildPVC),
				PVC:         config.PVCConfiguration(c.PVC),
			}
			for tmpl_n, tmpl_v := range ctpls {
				if _, ok := tpls[tmpl_n]; ok {
					return fmt.Errorf("duplicate workspace template %q in workspace class %q", tmpl_n, k)
				}
				tpls[tmpl_n] = tmpl_v
			}
		}
		schedulerName = ucfg.Workspace.SchedulerName
		if ucfg.Workspace.HostURL != "" {
			gitpodHostURL = ucfg.Workspace.HostURL
		}
		if ucfg.Workspace.WorkspaceClusterHost != "" {
			workspaceClusterHost = ucfg.Workspace.WorkspaceClusterHost
		}
		if ucfg.Workspace.WorkspaceURLTemplate != "" {
			workspaceURLTemplate = ucfg.Workspace.WorkspaceURLTemplate
		}
		if ucfg.Workspace.WorkspacePortURLTemplate != "" {
			workspacePortURLTemplate = ucfg.Workspace.WorkspacePortURLTemplate
		}
		rateLimits = ucfg.Workspace.WSManagerRateLimits

		return nil
	})
	if err != nil {
		return nil, err
	}

	var imageBuilderTLS struct {
		CA          string `json:"ca"`
		Certificate string `json:"crt"`
		PrivateKey  string `json:"key"`
	}
	if ctx.Config.Kind == configv1.InstallationWorkspace {
		// Image builder TLS is only enabled in workspace clusters. This check
		// can be removed once image-builder-mk3 has been removed from application clusters
		// (https://github.com/gitpod-io/gitpod/issues/7845).
		imageBuilderTLS = struct {
			CA          string `json:"ca"`
			Certificate string `json:"crt"`
			PrivateKey  string `json:"key"`
		}{
			CA:          "/image-builder-mk3-tls-certs/ca.crt",
			Certificate: "/image-builder-mk3-tls-certs/tls.crt",
			PrivateKey:  "/image-builder-mk3-tls-certs/tls.key",
		}
	}

	wsmcfg := config.ServiceConfiguration{
		Manager: config.Configuration{
			Namespace:      ctx.Namespace,
			SchedulerName:  schedulerName,
			SeccompProfile: fmt.Sprintf("workspace_default_%s.json", ctx.VersionManifest.Version),
			WorkspaceDaemon: config.WorkspaceDaemonConfiguration{
				Port: 8080,
				TLS: struct {
					Authority   string `json:"ca"`
					Certificate string `json:"crt"`
					PrivateKey  string `json:"key"`
				}{
					Authority:   "/ws-daemon-tls-certs/ca.crt",
					Certificate: "/ws-daemon-tls-certs/tls.crt",
					PrivateKey:  "/ws-daemon-tls-certs/tls.key",
				},
			},
			WorkspaceClasses:     classes,
			HeartbeatInterval:    util.Duration(30 * time.Second),
			GitpodHostURL:        gitpodHostURL,
			WorkspaceClusterHost: workspaceClusterHost,
			InitProbe: config.InitProbeConfiguration{
				Timeout: (1 * time.Second).String(),
			},
			WorkspaceURLTemplate:     workspaceURLTemplate,
			WorkspacePortURLTemplate: workspacePortURLTemplate,
			WorkspaceHostPath:        wsdaemon.HostWorkingArea,
			Timeouts: config.WorkspaceTimeoutConfiguration{
				AfterClose:          timeoutAfterClose,
				HeadlessWorkspace:   util.Duration(1 * time.Hour),
				Initialization:      util.Duration(30 * time.Minute),
				RegularWorkspace:    util.Duration(30 * time.Minute),
				MaxLifetime:         ctx.Config.Workspace.MaxLifetime,
				TotalStartup:        util.Duration(1 * time.Hour),
				ContentFinalization: util.Duration(1 * time.Hour),
				Stopping:            util.Duration(1 * time.Hour),
				Interrupted:         util.Duration(5 * time.Minute),
			},
			//EventTraceLog:                "", // todo(sje): make conditional based on config
			ReconnectionInterval:  util.Duration(30 * time.Second),
			RegistryFacadeHost:    fmt.Sprintf("reg.%s:%d", ctx.Config.Domain, common.RegistryFacadeServicePort),
			WorkspaceCACertSecret: customCASecret,
		},
		Content: struct {
			Storage storageconfig.StorageConfig `json:"storage"`
		}{Storage: common.StorageConfig(ctx)},
		RPCServer: struct {
			Addr string `json:"addr"`
			TLS  struct {
				CA          string `json:"ca"`
				Certificate string `json:"crt"`
				PrivateKey  string `json:"key"`
			} `json:"tls"`
			RateLimits map[string]grpc.RateLimit `json:"ratelimits"`
		}{
			Addr: fmt.Sprintf(":%d", RPCPort),
			TLS: struct {
				CA          string `json:"ca"`
				Certificate string `json:"crt"`
				PrivateKey  string `json:"key"`
			}{
				CA:          "/certs/ca.crt",
				Certificate: "/certs/tls.crt",
				PrivateKey:  "/certs/tls.key",
			},
			RateLimits: rateLimits,
		},
		ImageBuilderProxy: struct {
			TargetAddr string "json:\"targetAddr\""
			TLS        struct {
				CA          string `json:"ca"`
				Certificate string `json:"crt"`
				PrivateKey  string `json:"key"`
			} `json:"tls"`
		}{
			TargetAddr: fmt.Sprintf("%s.%s.svc.cluster.local:%d", common.ImageBuilderComponent, ctx.Namespace, common.ImageBuilderRPCPort),
			TLS:        imageBuilderTLS,
		},
		PProf: struct {
			Addr string `json:"addr"`
		}{Addr: common.LocalhostPprofAddr()},
		Prometheus: struct {
			Addr string `json:"addr"`
		}{Addr: common.LocalhostPrometheusAddr()},
	}

	fc, err := common.ToJSONString(wsmcfg)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal ws-manager config: %w", err)
	}

	res := []runtime.Object{
		&corev1.ConfigMap{
			TypeMeta: common.TypeMetaConfigmap,
			ObjectMeta: metav1.ObjectMeta{
				Name:        Component,
				Namespace:   ctx.Namespace,
				Labels:      common.CustomizeLabel(ctx, Component, common.TypeMetaConfigmap),
				Annotations: common.CustomizeAnnotation(ctx, Component, common.TypeMetaConfigmap),
			},
			Data: map[string]string{
				"config.json": string(fc),
			},
		},
		&corev1.ConfigMap{
			TypeMeta: common.TypeMetaConfigmap,
			ObjectMeta: metav1.ObjectMeta{
				Name:      WorkspaceTemplateConfigMap,
				Namespace: ctx.Namespace,
				Labels:    common.DefaultLabels(Component),
			},
			Data: tpls,
		},
	}
	return res, nil
}

func buildWorkspaceTemplates(ctx *common.RenderContext, cfgTpls *configv1.WorkspaceTemplates, className string) (config.WorkspacePodTemplateConfiguration, map[string]string, error) {
	var (
		cfg  config.WorkspacePodTemplateConfiguration
		tpls = make(map[string]string)
	)
	if cfgTpls == nil {
		cfgTpls = new(configv1.WorkspaceTemplates)
	}

	ops := []struct {
		Name string
		Path *string
		Tpl  *corev1.Pod
	}{
		{Name: "default", Path: &cfg.DefaultPath, Tpl: cfgTpls.Default},
		{Name: "imagebuild", Path: &cfg.ImagebuildPath, Tpl: cfgTpls.ImageBuild},
		{Name: "prebuild", Path: &cfg.PrebuildPath, Tpl: cfgTpls.Prebuild},
		{Name: "regular", Path: &cfg.RegularPath, Tpl: cfgTpls.Regular},
	}
	for _, op := range ops {
		if op.Tpl == nil {
			continue
		}
		fc, err := yaml.Marshal(op.Tpl)
		if err != nil {
			return cfg, nil, fmt.Errorf("unable to marshal %s workspace template: %w", op.Name, err)
		}
		fn := op.Name + ".yaml"
		if className != "" {
			fn = className + "-" + fn
		}
		*op.Path = filepath.Join(WorkspaceTemplatePath, fn)
		tpls[fn] = string(fc)
	}

	return cfg, tpls, nil
}
