/*
Copyright 2020 The Flux authors

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 main

import (
	"context"
	"fmt"
	"io/ioutil"

	"github.com/fluxcd/flux2/internal/flags"
	"github.com/fluxcd/flux2/internal/utils"
	"github.com/fluxcd/pkg/apis/meta"

	"github.com/spf13/cobra"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	apimeta "k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/wait"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/yaml"

	helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
)

var createHelmReleaseCmd = &cobra.Command{
	Use:     "helmrelease [name]",
	Aliases: []string{"hr"},
	Short:   "Create or update a HelmRelease resource",
	Long:    "The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.",
	Example: `  # Create a HelmRelease with a chart from a HelmRepository source
  flux create hr podinfo \
    --interval=10m \
    --source=HelmRepository/podinfo \
    --chart=podinfo \
    --chart-version=">4.0.0"

  # Create a HelmRelease with a chart from a GitRepository source
  flux create hr podinfo \
    --interval=10m \
    --source=GitRepository/podinfo \
    --chart=./charts/podinfo

  # Create a HelmRelease with a chart from a Bucket source
  flux create hr podinfo \
    --interval=10m \
    --source=Bucket/podinfo \
    --chart=./charts/podinfo

  # Create a HelmRelease with values from a local YAML file
  flux create hr podinfo \
    --source=HelmRepository/podinfo \
    --chart=podinfo \
    --values=./my-values.yaml

  # Create a HelmRelease with values from a Kubernetes secret
  kubectl -n app create secret generic my-secret-values \
	--from-file=values.yaml=/path/to/my-secret-values.yaml
  flux -n app create hr podinfo \
    --source=HelmRepository/podinfo \
    --chart=podinfo \
    --values-from=Secret/my-secret-values

  # Create a HelmRelease with a custom release name
  flux create hr podinfo \
    --release-name=podinfo-dev
    --source=HelmRepository/podinfo \
    --chart=podinfo \

  # Create a HelmRelease targeting another namespace than the resource
  flux create hr podinfo \
    --target-namespace=default \
    --source=HelmRepository/podinfo \
    --chart=podinfo

  # Create a HelmRelease definition on disk without applying it on the cluster
  flux create hr podinfo \
    --source=HelmRepository/podinfo \
    --chart=podinfo \
    --values=./values.yaml \
    --export > podinfo-release.yaml
`,
	RunE: createHelmReleaseCmdRun,
}

var (
	hrName            string
	hrSource          flags.HelmChartSource
	hrDependsOn       []string
	hrChart           string
	hrChartVersion    string
	hrTargetNamespace string
	hrValuesFile      string
	hrValuesFrom      flags.HelmReleaseValuesFrom
	hrSAName          string
)

func init() {
	createHelmReleaseCmd.Flags().StringVar(&hrName, "release-name", "", "name used for the Helm release, defaults to a composition of '[<target-namespace>-]<HelmRelease-name>'")
	createHelmReleaseCmd.Flags().Var(&hrSource, "source", hrSource.Description())
	createHelmReleaseCmd.Flags().StringVar(&hrChart, "chart", "", "Helm chart name or path")
	createHelmReleaseCmd.Flags().StringVar(&hrChartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)")
	createHelmReleaseCmd.Flags().StringArrayVar(&hrDependsOn, "depends-on", nil, "HelmReleases that must be ready before this release can be installed, supported formats '<name>' and '<namespace>/<name>'")
	createHelmReleaseCmd.Flags().StringVar(&hrTargetNamespace, "target-namespace", "", "namespace to install this release, defaults to the HelmRelease namespace")
	createHelmReleaseCmd.Flags().StringVar(&hrSAName, "service-account", "", "the name of the service account to impersonate when reconciling this HelmRelease")
	createHelmReleaseCmd.Flags().StringVar(&hrValuesFile, "values", "", "local path to the values.yaml file")
	createHelmReleaseCmd.Flags().Var(&hrValuesFrom, "values-from", hrValuesFrom.Description())
	createCmd.AddCommand(createHelmReleaseCmd)
}

func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
	if len(args) < 1 {
		return fmt.Errorf("HelmRelease name is required")
	}
	name := args[0]

	if hrChart == "" {
		return fmt.Errorf("chart name or path is required")
	}

	sourceLabels, err := parseLabels()
	if err != nil {
		return err
	}

	if !export {
		logger.Generatef("generating HelmRelease")
	}

	helmRelease := helmv2.HelmRelease{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
			Labels:    sourceLabels,
		},
		Spec: helmv2.HelmReleaseSpec{
			ReleaseName: hrName,
			DependsOn:   utils.MakeDependsOn(hrDependsOn),
			Interval: metav1.Duration{
				Duration: interval,
			},
			TargetNamespace: hrTargetNamespace,
			Chart: helmv2.HelmChartTemplate{
				Spec: helmv2.HelmChartTemplateSpec{
					Chart:   hrChart,
					Version: hrChartVersion,
					SourceRef: helmv2.CrossNamespaceObjectReference{
						Kind: hrSource.Kind,
						Name: hrSource.Name,
					},
				},
			},
			Suspend: false,
		},
	}

	if hrSAName != "" {
		helmRelease.Spec.ServiceAccountName = hrSAName
	}

	if hrValuesFile != "" {
		data, err := ioutil.ReadFile(hrValuesFile)
		if err != nil {
			return fmt.Errorf("reading values from %s failed: %w", hrValuesFile, err)
		}

		json, err := yaml.YAMLToJSON(data)
		if err != nil {
			return fmt.Errorf("converting values to JSON from %s failed: %w", hrValuesFile, err)
		}

		helmRelease.Spec.Values = &apiextensionsv1.JSON{Raw: json}
	}

	if hrValuesFrom.String() != "" {
		helmRelease.Spec.ValuesFrom = []helmv2.ValuesReference{{
			Kind: hrValuesFrom.Kind,
			Name: hrValuesFrom.Name,
		}}
	}

	if export {
		return exportHelmRelease(helmRelease)
	}

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
	if err != nil {
		return err
	}

	logger.Actionf("applying HelmRelease")
	namespacedName, err := upsertHelmRelease(ctx, kubeClient, &helmRelease)
	if err != nil {
		return err
	}

	logger.Waitingf("waiting for HelmRelease reconciliation")
	if err := wait.PollImmediate(pollInterval, timeout,
		isHelmReleaseReady(ctx, kubeClient, namespacedName, &helmRelease)); err != nil {
		return err
	}
	logger.Successf("HelmRelease %s is ready", name)

	logger.Successf("applied revision %s", helmRelease.Status.LastAppliedRevision)
	return nil
}

func upsertHelmRelease(ctx context.Context, kubeClient client.Client,
	helmRelease *helmv2.HelmRelease) (types.NamespacedName, error) {
	namespacedName := types.NamespacedName{
		Namespace: helmRelease.GetNamespace(),
		Name:      helmRelease.GetName(),
	}

	var existing helmv2.HelmRelease
	err := kubeClient.Get(ctx, namespacedName, &existing)
	if err != nil {
		if errors.IsNotFound(err) {
			if err := kubeClient.Create(ctx, helmRelease); err != nil {
				return namespacedName, err
			} else {
				logger.Successf("HelmRelease created")
				return namespacedName, nil
			}
		}
		return namespacedName, err
	}

	existing.Labels = helmRelease.Labels
	existing.Spec = helmRelease.Spec
	if err := kubeClient.Update(ctx, &existing); err != nil {
		return namespacedName, err
	}
	helmRelease = &existing
	logger.Successf("HelmRelease updated")
	return namespacedName, nil
}

func isHelmReleaseReady(ctx context.Context, kubeClient client.Client,
	namespacedName types.NamespacedName, helmRelease *helmv2.HelmRelease) wait.ConditionFunc {
	return func() (bool, error) {
		err := kubeClient.Get(ctx, namespacedName, helmRelease)
		if err != nil {
			return false, err
		}

		// Confirm the state we are observing is for the current generation
		if helmRelease.Generation != helmRelease.Status.ObservedGeneration {
			return false, nil
		}

		return apimeta.IsStatusConditionTrue(helmRelease.Status.Conditions, meta.ReadyCondition), nil
	}
}
