/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 platform

import (
	"context"
	"runtime"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
	"github.com/apache/camel-k/v2/pkg/client"
	"github.com/apache/camel-k/v2/pkg/kamelet/repository"
	"github.com/apache/camel-k/v2/pkg/util/defaults"
	"github.com/apache/camel-k/v2/pkg/util/log"
	"github.com/apache/camel-k/v2/pkg/util/openshift"
)

const (
	BuilderServiceAccount = "camel-k-builder"

	defaultBuildTimeout = 5 * time.Minute
)

// ConfigureDefaults fills with default values all missing details about the integration platform.
// Defaults are set in the status fields, not in the spec.
func ConfigureDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPlatform, verbose bool) error {
	// Reset the state to initial values
	p.ResyncStatusFullConfig()
	// Set the operator version controlling this resource
	p.Status.Version = defaults.Version
	// Apply settings from global integration platform that is bound to this operator
	if err := applyGlobalPlatformDefaults(ctx, c, p); err != nil {
		return err
	}

	if p.Status.Build.RuntimeProvider == "" {
		p.Status.Build.RuntimeProvider = v1.RuntimeProviderQuarkus
		log.Debugf("Integration Platform %s [%s]: setting default runtime provider %s", p.Name, p.Namespace, v1.RuntimeProviderQuarkus)
	}
	if p.Status.Build.RuntimeVersion == "" {
		p.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
		log.Debugf("Integration Platform %s [%s]: setting default runtime version %s", p.Name, p.Namespace, p.Status.Build.PublishStrategy)
	}

	// update missing fields in the resource
	if p.Status.Cluster == "" {
		log.Debugf("Integration Platform %s [%s]: setting cluster status", p.Name, p.Namespace)
		// determine the kind of cluster the platform is installed into
		isOpenShift, err := openshift.IsOpenShift(c)
		switch {
		case err != nil:
			return err
		case isOpenShift:
			p.Status.Cluster = v1.IntegrationPlatformClusterOpenShift
		default:
			p.Status.Cluster = v1.IntegrationPlatformClusterKubernetes
		}
	}

	if p.Status.Build.PublishStrategy == "" {
		p.Status.Build.PublishStrategy = v1.IntegrationPlatformBuildPublishStrategyJib
		log.Debugf("Integration Platform %s [%s]: setting publishing strategy %s", p.Name, p.Namespace, p.Status.Build.PublishStrategy)
	}

	if p.Status.Build.BuildConfiguration.Strategy == "" {
		defaultStrategy := v1.BuildStrategyRoutine
		p.Status.Build.BuildConfiguration.Strategy = defaultStrategy
		log.Debugf("Integration Platform %s [%s]: setting build strategy %s", p.Name, p.Namespace, p.Status.Build.BuildConfiguration.Strategy)
	}

	if p.Status.Build.BuildConfiguration.OrderStrategy == "" {
		p.Status.Build.BuildConfiguration.OrderStrategy = v1.BuildOrderStrategyDependencies
		log.Debugf("Integration Platform %s [%s]: setting build order strategy %s", p.Name, p.Namespace, p.Status.Build.BuildConfiguration.OrderStrategy)
	}

	err := setPlatformDefaults(p, verbose)
	if err != nil {
		return err
	}

	err = configureRegistry(ctx, c, p)
	if err != nil {
		return err
	}

	//nolint:staticcheck
	if verbose && p.Status.Build.PublishStrategy != v1.IntegrationPlatformBuildPublishStrategyS2I && p.Status.Build.Registry.Address == "" {
		log.Log.Info("No registry specified for publishing images")
	}

	if verbose && p.Status.Build.GetTimeout().Duration != 0 {
		log.Log.Infof("Maven Timeout set to %s", p.Status.Build.GetTimeout().Duration)
	}

	return nil
}

func configureRegistry(ctx context.Context, c client.Client, p *v1.IntegrationPlatform) error {
	//nolint:staticcheck
	if p.Status.Cluster == v1.IntegrationPlatformClusterOpenShift &&
		p.Status.Build.PublishStrategy == v1.IntegrationPlatformBuildPublishStrategyS2I &&
		p.Status.Build.Registry.Address == "" {
		err := configureForOpenShiftS2i(ctx, c, p)
		if err != nil {
			return err
		}
	}

	log.Debugf("Final Registry Address: %s", p.Status.Build.Registry.Address)

	return nil
}

func configureForOpenShiftS2i(ctx context.Context, c client.Client, p *v1.IntegrationPlatform) error {
	log.Debugf("Integration Platform %s [%s]: setting registry address", p.Name, p.Namespace)
	// Default to using OpenShift internal container images registry when using a strategy other than S2I
	p.Status.Build.Registry.Address = defaults.OpenShiftRegistryAddress

	// OpenShift automatically injects the service CA certificate into the service-ca.crt key on the ConfigMap
	cm, err := createServiceCaBundleConfigMap(ctx, c, p)
	if err != nil {
		return err
	}

	log.Debugf("Integration Platform %s [%s]: setting registry certificate authority", p.Name, p.Namespace)
	p.Status.Build.Registry.CA = cm.Name

	// Default to using the registry secret that's configured for the builder service account
	if p.Status.Build.Registry.Secret == "" {
		log.Debugf("Integration Platform %s [%s]: setting registry secret", p.Name, p.Namespace)
		// Bind the required role to push images to the registry
		err := createBuilderRegistryRoleBinding(ctx, c, p)
		if err != nil {
			return err
		}

		sa := corev1.ServiceAccount{}
		err = c.Get(ctx, types.NamespacedName{Namespace: p.Namespace, Name: BuilderServiceAccount}, &sa)
		if err != nil {
			return err
		}
		// We may want to read the secret keys instead of relying on the secret name scheme
		for _, secret := range sa.Secrets {
			if strings.Contains(secret.Name, "camel-k-builder-dockercfg") {
				p.Status.Build.Registry.Secret = secret.Name

				break
			}
		}
	}

	return nil
}

func applyGlobalPlatformDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPlatform) error {
	operatorNamespace := GetOperatorNamespace()
	if operatorNamespace != "" {
		operatorID := defaults.OperatorID()
		if operatorNamespace != p.Namespace || (operatorID != "" && p.Name != operatorID) {
			if globalPlatform, err := findLocal(ctx, c, operatorNamespace); err != nil && !k8serrors.IsNotFound(err) {
				return err
			} else if globalPlatform != nil {
				applyPlatformSpec(globalPlatform, p)
			}
		}
	}

	return nil
}

func applyPlatformSpec(source *v1.IntegrationPlatform, target *v1.IntegrationPlatform) {
	if target.Status.Cluster == "" {
		target.Status.Cluster = source.Status.Cluster
	}
	if target.Status.Profile == "" {
		log.Debugf("Integration Platform %s [%s]: setting profile", target.Name, target.Namespace)
		target.Status.Profile = source.Status.Profile
	}

	if target.Status.Build.PublishStrategy == "" {
		target.Status.Build.PublishStrategy = source.Status.Build.PublishStrategy
	}

	if target.Status.Build.BuildConfiguration.Strategy == "" {
		target.Status.Build.BuildConfiguration.Strategy = source.Status.Build.BuildConfiguration.Strategy
	}

	if target.Status.Build.BuildConfiguration.OrderStrategy == "" {
		target.Status.Build.BuildConfiguration.OrderStrategy = source.Status.Build.BuildConfiguration.OrderStrategy
	}

	if target.Status.Build.RuntimeVersion == "" {
		log.Debugf("Integration Platform %s [%s]: setting runtime version", target.Name, target.Namespace)
		target.Status.Build.RuntimeVersion = source.Status.Build.RuntimeVersion
	}
	if target.Status.Build.BaseImage == "" {
		log.Debugf("Integration Platform %s [%s]: setting base image", target.Name, target.Namespace)
		target.Status.Build.BaseImage = source.Status.Build.BaseImage
	}

	if target.Status.Build.Maven.LocalRepository == "" {
		log.Debugf("Integration Platform %s [%s]: setting local repository", target.Name, target.Namespace)
		target.Status.Build.Maven.LocalRepository = source.Status.Build.Maven.LocalRepository
	}

	if len(source.Status.Build.Maven.CLIOptions) > 0 && len(target.Status.Build.Maven.CLIOptions) == 0 {
		log.Debugf("Integration Platform %s [%s]: setting CLI options", target.Name, target.Namespace)
		target.Status.Build.Maven.CLIOptions = make([]string, len(source.Status.Build.Maven.CLIOptions))
		copy(target.Status.Build.Maven.CLIOptions, source.Status.Build.Maven.CLIOptions)
	}

	if len(source.Status.Build.Maven.Properties) > 0 {
		log.Debugf("Integration Platform %s [%s]: setting Maven properties", target.Name, target.Namespace)
		if len(target.Status.Build.Maven.Properties) == 0 {
			target.Status.Build.Maven.Properties = make(map[string]string, len(source.Status.Build.Maven.Properties))
		}

		for key, val := range source.Status.Build.Maven.Properties {
			// only set unknown properties on target
			if _, ok := target.Status.Build.Maven.Properties[key]; !ok {
				target.Status.Build.Maven.Properties[key] = val
			}
		}
	}

	if len(source.Status.Build.Maven.Extension) > 0 && len(target.Status.Build.Maven.Extension) == 0 {
		log.Debugf("Integration Platform %s [%s]: setting Maven extensions", target.Name, target.Namespace)
		target.Status.Build.Maven.Extension = make([]v1.MavenArtifact, len(source.Status.Build.Maven.Extension))
		copy(target.Status.Build.Maven.Extension, source.Status.Build.Maven.Extension)
	}

	if target.Status.Build.Registry.Address == "" && source.Status.Build.Registry.Address != "" {
		log.Debugf("Integration Platform %s [%s]: setting registry", target.Name, target.Namespace)
		source.Status.Build.Registry.DeepCopyInto(&target.Status.Build.Registry)
	}

	if err := target.Status.Traits.Merge(source.Status.Traits); err != nil {
		log.Errorf(err, "Integration Platform %s [%s]: failed to merge traits", target.Name, target.Namespace)
	} else if err := target.Status.Traits.Merge(target.Spec.Traits); err != nil {
		log.Errorf(err, "Integration Platform %s [%s]: failed to merge traits", target.Name, target.Namespace)
	}

	// Build timeout
	if target.Status.Build.Timeout == nil {
		log.Debugf("Integration Platform %s [%s]: setting build timeout", target.Name, target.Namespace)
		target.Status.Build.Timeout = source.Status.Build.Timeout
	}

	if target.Status.Build.MaxRunningBuilds <= 0 {
		log.Debugf("Integration Platform %s [%s]: setting max running builds", target.Name, target.Namespace)
		target.Status.Build.MaxRunningBuilds = source.Status.Build.MaxRunningBuilds
	}

	if len(target.Status.Kamelet.Repositories) == 0 {
		log.Debugf("Integration Platform %s [%s]: setting kamelet repositories", target.Name, target.Namespace)
		target.Status.Kamelet.Repositories = source.Status.Kamelet.Repositories
	}
}

func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
	if p.Status.Build.RuntimeVersion == "" {
		log.Debugf("Integration Platform %s [%s]: setting runtime version", p.Name, p.Namespace)
		p.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
	}
	if p.Status.Build.BaseImage == "" {
		log.Debugf("Integration Platform %s [%s]: setting base image", p.Name, p.Namespace)
		p.Status.Build.BaseImage = defaults.BaseImage()
	}
	if p.Status.Build.Maven.LocalRepository == "" {
		log.Debugf("Integration Platform %s [%s]: setting local repository", p.Name, p.Namespace)
		p.Status.Build.Maven.LocalRepository = defaults.LocalRepository
	}
	if len(p.Status.Build.Maven.CLIOptions) == 0 {
		log.Debugf("Integration Platform %s [%s]: setting CLI options", p.Name, p.Namespace)
		p.Status.Build.Maven.CLIOptions = []string{
			"-V",
			"--no-transfer-progress",
			"-Dstyle.color=never",
		}
	}

	// Build timeout
	if p.Status.Build.GetTimeout().Duration == 0 {
		p.Status.Build.Timeout = &metav1.Duration{
			Duration: defaultBuildTimeout,
		}
	} else {
		d := p.Status.Build.GetTimeout().Truncate(time.Second)

		if verbose && p.Status.Build.GetTimeout().Duration != d {
			log.Log.Infof("Build timeout minimum unit is sec (configured: %s, truncated: %s)", p.Status.Build.GetTimeout().Duration, d)
		}

		log.Debugf("Integration Platform %s [%s]: setting build timeout", p.Name, p.Namespace)
		p.Status.Build.Timeout = &metav1.Duration{
			Duration: d,
		}
	}

	if p.Status.Build.MaxRunningBuilds <= 0 {
		log.Debugf("Integration Platform %s [%s]: setting max running builds", p.Name, p.Namespace)
		switch p.Status.Build.BuildConfiguration.Strategy {
		case v1.BuildStrategyRoutine:
			p.Status.Build.MaxRunningBuilds = 3
		case v1.BuildStrategyPod:
			p.Status.Build.MaxRunningBuilds = 10
		}
	}

	if len(p.Status.Kamelet.Repositories) == 0 {
		log.Debugf("Integration Platform %s [%s]: setting kamelet repositories", p.Name, p.Namespace)
		p.Status.Kamelet.Repositories = append(p.Status.Kamelet.Repositories, v1.KameletRepositorySpec{
			URI: repository.DefaultRemoteRepository,
		})
	}
	setStatusAdditionalInfo(p)

	buildConfig := &p.Status.Build.BuildConfiguration
	if buildConfig.ImagePlatforms == nil {
		if runtime.GOARCH == "arm64" {
			buildConfig.ImagePlatforms = []string{"linux/arm64"}
		}
	}

	if verbose {
		log.Log.Infof("RuntimeVersion set to %s", p.Status.Build.RuntimeVersion)
		log.Log.Infof("BaseImage set to %s", p.Status.Build.BaseImage)
		log.Log.Infof("ImagePlatforms set to %s", buildConfig.ImagePlatforms)
		log.Log.Infof("LocalRepository set to %s", p.Status.Build.Maven.LocalRepository)
		log.Log.Infof("Timeout set to %s", p.Status.Build.GetTimeout())
	}

	return nil
}

func setStatusAdditionalInfo(platform *v1.IntegrationPlatform) {
	platform.Status.Info = make(map[string]string)
	log.Debugf("Integration Platform %s [%s]: setting status info", platform.Name, platform.Namespace)
	platform.Status.Info["goVersion"] = runtime.Version()
	platform.Status.Info["goOS"] = runtime.GOOS
	platform.Status.Info["gitCommit"] = defaults.GitCommit
}

func createServiceCaBundleConfigMap(ctx context.Context, client client.Client, p *v1.IntegrationPlatform) (*corev1.ConfigMap, error) {
	cm := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      BuilderServiceAccount + "-ca",
			Namespace: p.Namespace,
			Annotations: map[string]string{
				"service.beta.openshift.io/inject-cabundle": "true",
			},
		},
	}

	err := client.Create(ctx, cm)
	if err != nil && !k8serrors.IsAlreadyExists(err) {
		return nil, err
	}

	return cm, nil
}

func createBuilderRegistryRoleBinding(ctx context.Context, client client.Client, p *v1.IntegrationPlatform) error {
	rb := &rbacv1.RoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name:      BuilderServiceAccount + "-registry",
			Namespace: p.Namespace,
		},
		Subjects: []rbacv1.Subject{
			{
				Kind: "ServiceAccount",
				Name: BuilderServiceAccount,
			},
		},
		RoleRef: rbacv1.RoleRef{
			Kind:     "ClusterRole",
			APIGroup: "rbac.authorization.k8s.io",
			Name:     "system:image-builder",
		},
	}

	err := client.Create(ctx, rb)
	if err != nil {
		if k8serrors.IsForbidden(err) {
			log.Log.Infof("Cannot grant permission to push images to the registry. "+
				"Run 'oc policy add-role-to-user system:image-builder system:serviceaccount:%s:%s' as a system admin.", p.Namespace, BuilderServiceAccount)
		} else if !k8serrors.IsAlreadyExists(err) {
			return err
		}
	}

	return nil
}
