//
// Copyright (c) 2019-2021 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package workspace

import (
	"context"
	"fmt"

	dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
	"github.com/google/go-cmp/cmp"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	runtimeClient "sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/devfile/devworkspace-operator/pkg/common"
	"github.com/devfile/devworkspace-operator/pkg/config"
)

type ServiceAcctProvisioningStatus struct {
	ProvisioningStatus
	ServiceAccountName string
}

func SyncServiceAccount(
	workspace *dw.DevWorkspace,
	additionalAnnotations map[string]string,
	clusterAPI ClusterAPI) ServiceAcctProvisioningStatus {
	// note: autoMountServiceAccount := true comes from a hardcoded value in prerequisites.go
	autoMountServiceAccount := true
	saName := common.ServiceAccountName(workspace.Status.DevWorkspaceId)

	specSA := corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Name:      saName,
			Namespace: workspace.Namespace,
		},
		AutomountServiceAccountToken: &autoMountServiceAccount,
	}

	if len(additionalAnnotations) > 0 {
		specSA.Annotations = map[string]string{}
		for annotKey, annotVal := range additionalAnnotations {
			specSA.Annotations[annotKey] = annotVal
		}
	}

	err := controllerutil.SetControllerReference(workspace, &specSA, clusterAPI.Scheme)
	if err != nil {
		return ServiceAcctProvisioningStatus{
			ProvisioningStatus: ProvisioningStatus{
				Err: err,
			},
		}
	}
	clusterSA, err := getClusterSA(specSA, clusterAPI.Client)
	if err != nil {
		return ServiceAcctProvisioningStatus{
			ProvisioningStatus: ProvisioningStatus{
				Err: err,
			},
		}
	}

	if clusterSA == nil {
		clusterAPI.Logger.Info("Creating DevWorkspace ServiceAccount")
		err := clusterAPI.Client.Create(context.TODO(), &specSA)
		return ServiceAcctProvisioningStatus{
			ProvisioningStatus: ProvisioningStatus{
				Continue: false,
				Requeue:  true,
				Err:      err,
			},
		}
	}

	if !cmp.Equal(specSA.Annotations, clusterSA.Annotations) {
		clusterAPI.Logger.Info("Updating DevWorkspace ServiceAccount")
		if config.ExperimentalFeaturesEnabled() {
			clusterAPI.Logger.Info(fmt.Sprintf("Diff: %s", cmp.Diff(specSA.Annotations, clusterSA.Annotations)))
		}
		patch := runtimeClient.MergeFrom(&specSA)
		err := clusterAPI.Client.Patch(context.TODO(), clusterSA, patch)
		if err != nil {
			if errors.IsConflict(err) {
				return ServiceAcctProvisioningStatus{ProvisioningStatus: ProvisioningStatus{Requeue: true}}
			}
			return ServiceAcctProvisioningStatus{ProvisioningStatus: ProvisioningStatus{Err: err}}
		}
		return ServiceAcctProvisioningStatus{
			ProvisioningStatus: ProvisioningStatus{Requeue: true},
		}
	}

	return ServiceAcctProvisioningStatus{
		ProvisioningStatus: ProvisioningStatus{
			Continue: true,
		},
		ServiceAccountName: saName,
	}
}

func getClusterSA(sa corev1.ServiceAccount, client runtimeClient.Client) (*corev1.ServiceAccount, error) {
	clusterSA := &corev1.ServiceAccount{}
	namespacedName := types.NamespacedName{
		Name:      sa.Name,
		Namespace: sa.Namespace,
	}
	err := client.Get(context.TODO(), namespacedName, clusterSA)
	if err != nil {
		if errors.IsNotFound(err) {
			return nil, nil
		}
		return nil, err
	}
	return clusterSA, nil
}
