// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// 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 cmd

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/yaml"

	"github.com/aws/eks-anywhere/pkg/semver"
	"github.com/aws/eks-anywhere/release/cli/pkg/aws/s3"
	"github.com/aws/eks-anywhere/release/cli/pkg/bundles"
	"github.com/aws/eks-anywhere/release/cli/pkg/clients"
	"github.com/aws/eks-anywhere/release/cli/pkg/constants"
	"github.com/aws/eks-anywhere/release/cli/pkg/filereader"
	"github.com/aws/eks-anywhere/release/cli/pkg/operations"
	releasetypes "github.com/aws/eks-anywhere/release/cli/pkg/types"
	releaseutils "github.com/aws/eks-anywhere/release/cli/pkg/util/release"
)

const maxReleasesInDevManifest = 20

var (
	bundleReleaseManifestFile = "/bundle-release.yaml"
	eksAReleaseManifestFile   = "/eks-a-release.yaml"
)

// releaseCmd represents the release command.
var releaseCmd = &cobra.Command{
	Use:   "release",
	Short: "Cut an eks-anywhere release",
	PreRun: func(cmd *cobra.Command, args []string) {
		err := viper.BindPFlags(cmd.Flags())
		if err != nil {
			fmt.Printf("Error initializing flags: %v\n", err)
			os.Exit(1)
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		// TODO validation on these flags
		releaseVersion := viper.GetString("release-version")
		bundleNumber := viper.GetInt("bundle-number")
		cliMinVersion := viper.GetString("min-version")
		cliMaxVersion := viper.GetString("max-version")
		releaseNumber := viper.GetInt("release-number")
		cliRepoDir := viper.GetString("cli-repo-source")
		buildRepoDir := viper.GetString("build-repo-source")
		cliRepoUrl := viper.GetString("cli-repo-url")
		buildRepoUrl := viper.GetString("build-repo-url")
		buildRepoBranchName := viper.GetString("build-repo-branch-name")
		cliRepoBranchName := viper.GetString("cli-repo-branch-name")
		artifactDir := viper.GetString("artifact-dir")
		sourceBucket := viper.GetString("source-bucket")
		releaseBucket := viper.GetString("release-bucket")
		sourceContainerRegistry := viper.GetString("source-container-registry")
		packagesSourceContainerRegistry := viper.GetString("packages-source-container-registry")
		releaseContainerRegistry := viper.GetString("release-container-registry")
		packagesReleaseContainerRegistry := viper.GetString("packages-release-container-registry")
		cdn := viper.GetString("cdn")
		devRelease := viper.GetBool("dev-release")
		dryRun := viper.GetBool("dry-run")
		weekly := viper.GetBool("weekly")
		releaseTime := time.Now().UTC()
		releaseDate := releaseTime.Format(constants.YYYYMMDD)
		awsSignerProfileArn := viper.GetString("aws-signer-profile-arn")

		var bundleRelease bool
		var releaseEnvironment string

		if !devRelease {
			bundleRelease = viper.GetBool("bundle-release")
			releaseEnvironment = viper.GetString("release-environment")
		}

		if bundleRelease {
			releaseVersion = cliMaxVersion
		}

		releaseConfig := &releasetypes.ReleaseConfig{
			CliRepoSource:                    cliRepoDir,
			BuildRepoSource:                  buildRepoDir,
			CliRepoUrl:                       cliRepoUrl,
			BuildRepoUrl:                     buildRepoUrl,
			BuildRepoBranchName:              buildRepoBranchName,
			CliRepoBranchName:                cliRepoBranchName,
			ArtifactDir:                      artifactDir,
			SourceBucket:                     sourceBucket,
			ReleaseBucket:                    releaseBucket,
			SourceContainerRegistry:          sourceContainerRegistry,
			PackagesSourceContainerRegistry:  packagesSourceContainerRegistry,
			ReleaseContainerRegistry:         releaseContainerRegistry,
			PackagesReleaseContainerRegistry: packagesReleaseContainerRegistry,
			CDN:                              cdn,
			BundleNumber:                     bundleNumber,
			ReleaseNumber:                    releaseNumber,
			ReleaseVersion:                   releaseVersion,
			ReleaseDate:                      releaseDate,
			ReleaseTime:                      releaseTime,
			DevRelease:                       devRelease,
			BundleRelease:                    bundleRelease,
			DryRun:                           dryRun,
			Weekly:                           weekly,
			ReleaseEnvironment:               releaseEnvironment,
			AwsSignerProfileArn:              awsSignerProfileArn,
			MaxReleasesInManifest:            -1,
		}

		err := operations.SetRepoHeads(releaseConfig)
		if err != nil {
			fmt.Printf("Error getting heads of code repositories: %v\n", err)
			os.Exit(1)
		}

		var sourceClients *clients.SourceClients
		var releaseClients *clients.ReleaseClients
		if devRelease {
			sourceClients, releaseClients, err = clients.CreateDevReleaseClients(dryRun)
			if err != nil {
				fmt.Printf("Error creating clients: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("%s Successfully created dev release clients\n", constants.SuccessIcon)
		}
		if releaseEnvironment == "development" {
			sourceClients, releaseClients, err = clients.CreateStagingReleaseClients(bundleRelease)
			if err != nil {
				fmt.Printf("Error creating clients: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("%s Successfully created staging release clients\n", constants.SuccessIcon)
		}
		if releaseEnvironment == "production" {
			sourceClients, releaseClients, err = clients.CreateProdReleaseClients()
			if err != nil {
				fmt.Printf("Error creating clients: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("%s Successfully created dev release clients\n", constants.SuccessIcon)
		}

		releaseConfig.SourceClients = sourceClients
		releaseConfig.ReleaseClients = releaseClients

		if devRelease {
			buildNumber, err := filereader.GetNextEksADevBuildNumber(releaseVersion, releaseConfig)
			if err != nil {
				fmt.Printf("Error getting next EKS-A dev release build number: %v\n", err)
				os.Exit(1)
			}
			releaseVersion, err = filereader.GetCurrentEksADevReleaseVersion(releaseVersion, releaseConfig, buildNumber)
			if err != nil {
				fmt.Printf("Error getting current EKS-A dev release version: %v\n", err)
				os.Exit(1)
			}
			releaseConfig.BundleNumber = buildNumber
			releaseConfig.ReleaseVersion = releaseVersion
			releaseConfig.MaxReleasesInManifest = maxReleasesInDevManifest
		}
		releaseConfig.DevReleaseUriVersion = strings.ReplaceAll(releaseVersion, "+", "-")

		if devRelease || bundleRelease {
			bundle := bundles.NewBaseBundles(releaseConfig)
			bundle.Spec.CliMinVersion = cliMinVersion
			bundle.Spec.CliMaxVersion = cliMaxVersion

			bundleArtifactsTable, err := operations.GenerateBundleArtifactsTable(releaseConfig)
			if err != nil {
				fmt.Printf("Error getting bundle artifacts data: %v\n", err)
				os.Exit(1)
			}
			releaseConfig.BundleArtifactsTable = bundleArtifactsTable

			// Download ECR images + S3 artifacts and rename them to the
			// proper release URIs + Upload them to release destinations
			err = operations.BundleArtifactsRelease(releaseConfig)
			if err != nil {
				fmt.Printf("Error releasing bundle artifacts: %v\n", err)
				os.Exit(1)
			}

			imageDigests, err := operations.GenerateImageDigestsTable(context.Background(), releaseConfig)
			if err != nil {
				fmt.Printf("Error generating image digests table: %+v\n", err)
				os.Exit(1)
			}

			if bundleRelease && releaseEnvironment == "development" {
				err = operations.SignImagesNotation(releaseConfig, imageDigests)
				if err != nil {
					fmt.Printf("Error signing container images using notation CLI and AWS Signer: %v\n", err)
					os.Exit(1)
				}
			}

			if bundleRelease && releaseEnvironment == "production" {
				err = operations.CopyImageSignatureUsingOras(releaseConfig, imageDigests)
				if err != nil {
					fmt.Printf("Error copying image signature: %v\n", err)
					os.Exit(1)
				}
			}

			err = operations.GenerateBundleSpec(releaseConfig, bundle, imageDigests)
			if err != nil {
				fmt.Printf("Error generating bundles manifest: %+v\n", err)
				os.Exit(1)
			}

			err = operations.SignBundleManifest(context.Background(), bundle)
			if err != nil {
				fmt.Printf("Error signing bundles manifest: %+v\n", err)
				os.Exit(1)
			}

			err = operations.SignEKSDistroManifest(context.Background(), bundle)
			if err != nil {
				fmt.Printf("Error signing eks distro manifest: %+v\n", err)
				os.Exit(1)
			}

			bundleManifest, err := yaml.Marshal(bundle)
			if err != nil {
				fmt.Printf("Error marshaling bundles manifest: %+v\n", err)
				os.Exit(1)
			}
			fmt.Printf("\n%s\n", string(bundleManifest))

			if !dryRun {
				err = os.WriteFile(bundleReleaseManifestFile, bundleManifest, 0o644)
				if err != nil {
					fmt.Printf("Error writing bundles manifest file to disk: %v\n", err)
					os.Exit(1)
				}

				bundleReleaseManifestKey := releaseConfig.BundlesManifestFilepath()
				err = s3.UploadFile(bundleReleaseManifestFile, aws.String(releaseConfig.ReleaseBucket), aws.String(bundleReleaseManifestKey), releaseConfig.ReleaseClients.S3.Uploader, false)
				if err != nil {
					fmt.Printf("Error uploading bundle manifest to release bucket: %+v", err)
					os.Exit(1)
				}
				fmt.Printf("%s Successfully completed bundle release\n", constants.SuccessIcon)
			}

		}

		if devRelease || !bundleRelease {
			release, err := releaseutils.GetPreviousReleaseIfExists(releaseConfig)
			if err != nil {
				fmt.Printf("Error getting previous EKS-A releases: %v\n", err)
				os.Exit(1)
			}

			release.Name = "eks-anywhere"
			release.APIVersion = "anywhere.eks.amazonaws.com/v1alpha1"
			release.Kind = constants.ReleaseKind
			release.CreationTimestamp = v1.Time{Time: releaseTime}

			if devRelease {
				release.Spec.LatestVersion = releaseVersion
			} else {
				previousLatestVersion := release.Spec.LatestVersion
				previousLatestVersionSemver, err := semver.New(previousLatestVersion)
				if err != nil {
					fmt.Printf("Error getting semver for previous latest release version %s: %v\n", previousLatestVersion, err)
					os.Exit(1)
				}

				releaseVersionSemver, err := semver.New(releaseVersion)
				if err != nil {
					fmt.Printf("Error getting semver for current release version %s: %v\n", releaseVersion, err)
					os.Exit(1)
				}

				if releaseVersionSemver.GreaterThan(previousLatestVersionSemver) {
					release.Spec.LatestVersion = releaseVersion
				}
			}

			eksAArtifactsTable, err := operations.GenerateEksAArtifactsTable(releaseConfig)
			if err != nil {
				fmt.Printf("Error getting EKS-A artifacts data: %v\n", err)
				os.Exit(1)
			}
			releaseConfig.EksAArtifactsTable = eksAArtifactsTable

			err = operations.EksAArtifactsRelease(releaseConfig)
			if err != nil {
				fmt.Printf("Error releasing EKS-A CLI artifacts: %v\n", err)
				os.Exit(1)
			}

			currentEksARelease, err := bundles.GetEksARelease(releaseConfig)
			if err != nil {
				fmt.Printf("Error getting EKS-A release: %v\n", err)
				os.Exit(1)
			}

			currentEksAReleaseYaml, err := yaml.Marshal(currentEksARelease)
			if err != nil {
				fmt.Printf("Error marshaling EKS-A releases manifest: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("\n%s\n", string(currentEksAReleaseYaml))

			if dryRun {
				fmt.Printf("%s Successfully completed dry-run of release process\n", constants.SuccessIcon)
				os.Exit(0)
			}

			previousReleases := releaseutils.EksAReleases(release.Spec.Releases)
			release.Spec.Releases = previousReleases.AppendOrUpdateRelease(currentEksARelease)
			release.Spec.Releases = releaseutils.Trim(release.Spec.Releases, releaseConfig.MaxReleasesInManifest)

			releaseManifest, err := yaml.Marshal(release)
			if err != nil {
				fmt.Printf("Error marshaling EKS-A releases manifest: %v\n", err)
				os.Exit(1)
			}

			// Push the manifest file and other artifacts to release locations
			err = os.WriteFile(eksAReleaseManifestFile, releaseManifest, 0o644)
			if err != nil {
				fmt.Printf("Error writing EKS-A release manifest file to disk: %v\n", err)
				os.Exit(1)
			}

			eksAReleaseManifestKey := releaseConfig.ReleaseManifestFilepath()
			err = s3.UploadFile(eksAReleaseManifestFile, aws.String(releaseConfig.ReleaseBucket), aws.String(eksAReleaseManifestKey), releaseConfig.ReleaseClients.S3.Uploader, false)
			if err != nil {
				fmt.Printf("Error uploading EKS-A release manifest to release bucket: %v", err)
				os.Exit(1)
			}

			if !weekly {
				err = filereader.PutEksAReleaseVersion(releaseVersion, releaseConfig)
				if err != nil {
					fmt.Printf("Error uploading latest EKS-A release version to S3: %v\n", err)
					os.Exit(1)
				}
			}
			fmt.Printf("%s Successfully completed EKS-A release\n", constants.SuccessIcon)
		}
	},
}

func init() {
	rootCmd.AddCommand(releaseCmd)

	releaseCmd.Flags().String("release-version", "vDev", "The version of eks-a")
	releaseCmd.Flags().Int("bundle-number", 1, "The bundle version number")
	releaseCmd.Flags().String("min-version", "v0.0.0", "The minimum version of eks-a supported by dependency bundles")
	releaseCmd.Flags().String("max-version", "v0.0.0", "The maximum version of eks-a supported by dependency bundles")
	releaseCmd.Flags().Int("release-number", 1, "The release-number to create")
	releaseCmd.Flags().String("cli-repo-url", "", "URL to clone the eks-anywhere repo")
	releaseCmd.Flags().String("build-repo-url", "", "URL to clone the eks-anywhere-build-tooling repo")
	releaseCmd.Flags().String("cli-repo-source", "", "The eks-anywhere-cli source")
	releaseCmd.Flags().String("build-repo-source", "", "The eks-anywhere-build-tooling source")
	releaseCmd.Flags().String("build-repo-branch-name", "main", "The branch name to build bundles from")
	releaseCmd.Flags().String("cli-repo-branch-name", "main", "The branch name to build EKS-A CLI from")
	releaseCmd.Flags().String("artifact-dir", "downloaded-artifacts", "The base directory for artifacts")
	releaseCmd.Flags().String("cdn", "https://anywhere.eks.amazonaws.com", "The URL base for artifacts")
	releaseCmd.Flags().String("source-bucket", "eks-a-source-bucket", "The bucket name where the built/staging artifacts are located to download")
	releaseCmd.Flags().String("release-bucket", "eks-a-release-bucket", "The bucket name where released artifacts live")
	releaseCmd.Flags().String("source-container-registry", "", "The container registry to pull images from for a dev release")
	releaseCmd.Flags().String("packages-source-container-registry", "", "The container registry to pull packages images from for a dev release")
	releaseCmd.Flags().String("release-container-registry", "", "The container registry that images wll be pushed to")
	releaseCmd.Flags().String("packages-release-container-registry", "", "The container registry to push packages images to for a dev release")
	releaseCmd.Flags().Bool("dev-release", true, "Flag to indicate a dev release")
	releaseCmd.Flags().Bool("bundle-release", true, "Flag to indicate a bundle release")
	releaseCmd.Flags().String("release-environment", "", "Release environment")
	releaseCmd.Flags().Bool("dry-run", false, "Flag to indicate if the release is a dry run")
	releaseCmd.Flags().Bool("weekly", false, "Flag to indicate a weekly bundle release")
	releaseCmd.Flags().String("aws-signer-profile-arn", "", "Arn of AWS Signer profile to sign the container images")
}
