// 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 images

import (
	"fmt"
	"io"
	"net/http"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	docker "github.com/fsouza/go-dockerclient"
	"github.com/pkg/errors"
	"sigs.k8s.io/yaml"

	anywherev1alpha1 "github.com/aws/eks-anywhere/release/api/v1alpha1"
	assettypes "github.com/aws/eks-anywhere/release/pkg/assets/types"
	"github.com/aws/eks-anywhere/release/pkg/aws/ecr"
	"github.com/aws/eks-anywhere/release/pkg/aws/ecrpublic"
	"github.com/aws/eks-anywhere/release/pkg/aws/s3"
	"github.com/aws/eks-anywhere/release/pkg/constants"
	"github.com/aws/eks-anywhere/release/pkg/filereader"
	"github.com/aws/eks-anywhere/release/pkg/retrier"
	releasetypes "github.com/aws/eks-anywhere/release/pkg/types"
	artifactutils "github.com/aws/eks-anywhere/release/pkg/util/artifacts"
	commandutils "github.com/aws/eks-anywhere/release/pkg/util/command"
)

func PollForExistence(devRelease bool, authConfig *docker.AuthConfiguration, imageUri, imageContainerRegistry, releaseEnvironment, branchName string) error {
	repository, tag := artifactutils.SplitImageUri(imageUri, imageContainerRegistry)

	var requestUrl string
	if devRelease || releaseEnvironment == "development" {
		requestUrl = fmt.Sprintf("https://%s:%s@%s/v2/%s/manifests/%s", authConfig.Username, authConfig.Password, imageContainerRegistry, repository, tag)
	} else {
		requestUrl = fmt.Sprintf("https://%s:%s@public.ecr.aws/v2/%s/%s/manifests/%s", authConfig.Username, authConfig.Password, filepath.Base(imageContainerRegistry), repository, tag)
	}

	// Creating new GET request
	req, err := http.NewRequest("GET", requestUrl, nil)
	if err != nil {
		return errors.Cause(err)
	}

	// Retrier for downloading source ECR images. This retrier has a max timeout of 60 minutes. It
	// checks whether the error occured during download is an ImageNotFound error and retries the
	// download operation for a maximum of 60 retries, with a wait time of 30 seconds per retry.
	retrier := retrier.NewRetrier(60*time.Minute, retrier.WithRetryPolicy(func(totalRetries int, err error) (retry bool, wait time.Duration) {
		if branchName == "main" && artifactutils.IsImageNotFoundError(err) && totalRetries < 60 {
			return true, 30 * time.Second
		}
		return false, 0
	}))

	err = retrier.Retry(func() error {
		var err error
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}
		defer resp.Body.Close()

		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return err
		}

		bodyStr := string(body)
		if strings.Contains(bodyStr, "MANIFEST_UNKNOWN") {
			return fmt.Errorf("Requested image not found")
		}

		return nil
	})
	if err != nil {
		return fmt.Errorf("retries exhausted waiting for source image %s to be available for copy: %v", imageUri, err)
	}

	return nil
}

func CopyToDestination(sourceAuthConfig, releaseAuthConfig *docker.AuthConfiguration, sourceImageUri, releaseImageUri string) error {
	retrier := retrier.NewRetrier(60*time.Minute, retrier.WithRetryPolicy(func(totalRetries int, err error) (retry bool, wait time.Duration) {
		if err != nil && totalRetries < 10 {
			return true, 30 * time.Second
		}
		return false, 0
	}))

	sourceRegistryUsername := sourceAuthConfig.Username
	sourceRegistryPassword := sourceAuthConfig.Password
	releaseRegistryUsername := releaseAuthConfig.Username
	releaseRegistryPassword := releaseAuthConfig.Password
	err := retrier.Retry(func() error {
		cmd := exec.Command("skopeo", "copy", "--src-creds", fmt.Sprintf("%s:%s", sourceRegistryUsername, sourceRegistryPassword), "--dest-creds", fmt.Sprintf("%s:%s", releaseRegistryUsername, releaseRegistryPassword), fmt.Sprintf("docker://%s", sourceImageUri), fmt.Sprintf("docker://%s", releaseImageUri), "-f", "oci", "--all")
		out, err := commandutils.ExecCommand(cmd)
		fmt.Println(out)
		if err != nil {
			return fmt.Errorf("executing skopeo copy command: %v", err)
		}

		return nil
	})
	if err != nil {
		return fmt.Errorf("retries exhausted performing image copy from source to destination: %v", err)
	}

	return nil
}

func GetSourceImageURI(r *releasetypes.ReleaseConfig, name, repoName string, tagOptions map[string]string, imageTagConfiguration assettypes.ImageTagConfiguration, trimVersionSignifier, hasSeparateTagPerReleaseBranch bool) (string, string, error) {
	var sourceImageUri string
	var latestTag string
	var err error
	sourcedFromBranch := r.BuildRepoBranchName
	if r.DevRelease || r.ReleaseEnvironment == "development" {
		latestTag = artifactutils.GetLatestUploadDestination(r.BuildRepoBranchName)
		if imageTagConfiguration.SourceLatestTagFromECR && !r.DryRun {
			if (strings.Contains(name, "eks-anywhere-packages") || strings.Contains(name, "ecr-token-refresher")) && r.BuildRepoBranchName != "main" {
				latestTag, _, err = ecr.FilterECRRepoByTagPrefix(r.SourceClients.ECR.EcrClient, repoName, "v0.0.0", false)
			} else {
				latestTag, err = ecr.GetLatestImageSha(r.SourceClients.ECR.EcrClient, repoName)
			}
			if err != nil {
				return "", "", errors.Cause(err)
			}
		}
		if imageTagConfiguration.NonProdSourceImageTagFormat != "" {
			sourceImageTagPrefix := generateFormattedTagPrefix(imageTagConfiguration.NonProdSourceImageTagFormat, tagOptions)
			sourceImageUri = fmt.Sprintf("%s/%s:%s-%s",
				r.SourceContainerRegistry,
				repoName,
				sourceImageTagPrefix,
				latestTag,
			)
		} else {
			sourceImageUri = fmt.Sprintf("%s/%s:%s",
				r.SourceContainerRegistry,
				repoName,
				latestTag,
			)
		}
		if strings.HasSuffix(name, "-helm") || strings.HasSuffix(name, "-chart") {
			sourceImageUri += "-helm"
		}
		if trimVersionSignifier {
			sourceImageUri = strings.ReplaceAll(sourceImageUri, ":v", ":")
		}
		if !r.DryRun {
			sourceEcrAuthConfig := r.SourceClients.ECR.AuthConfig
			err := PollForExistence(r.DevRelease, sourceEcrAuthConfig, sourceImageUri, r.SourceContainerRegistry, r.ReleaseEnvironment, r.BuildRepoBranchName)
			if err != nil {
				if r.BuildRepoBranchName != "main" {
					fmt.Printf("Tag corresponding to %s branch not found for %s image. Using image artifact from main\n", r.BuildRepoBranchName, repoName)
					var gitTagFromMain string
					if strings.Contains(name, "bottlerocket-bootstrap") {
						gitTagFromMain = "non-existent"
					} else {
						gitTagPath := tagOptions["projectPath"]
						if hasSeparateTagPerReleaseBranch {
							gitTagPath = filepath.Join(tagOptions["projectPath"], tagOptions["eksDReleaseChannel"])
						}
						gitTagFromMain, err = filereader.ReadGitTag(gitTagPath, r.BuildRepoSource, "main")
						if err != nil {
							return "", "", errors.Cause(err)
						}
					}
					sourceImageUri = strings.NewReplacer(r.BuildRepoBranchName, "latest", tagOptions["gitTag"], gitTagFromMain).Replace(sourceImageUri)
					sourcedFromBranch = "main"
				} else {
					return "", "", errors.Cause(err)
				}
			}
		}
	} else if r.ReleaseEnvironment == "production" {
		if imageTagConfiguration.ProdSourceImageTagFormat != "" {
			sourceImageTagPrefix := generateFormattedTagPrefix(imageTagConfiguration.ProdSourceImageTagFormat, tagOptions)
			sourceImageUri = fmt.Sprintf("%s/%s:%s-eks-a-%d",
				r.SourceContainerRegistry,
				repoName,
				sourceImageTagPrefix,
				r.BundleNumber,
			)
		} else {
			sourceImageUri = fmt.Sprintf("%s/%s:%s-eks-a-%d",
				r.SourceContainerRegistry,
				repoName,
				tagOptions["gitTag"],
				r.BundleNumber,
			)
		}
		if trimVersionSignifier {
			sourceImageUri = strings.ReplaceAll(sourceImageUri, ":v", ":")
		}
	}

	return sourceImageUri, sourcedFromBranch, nil
}

func GetReleaseImageURI(r *releasetypes.ReleaseConfig, name, repoName string, tagOptions map[string]string, imageTagConfiguration assettypes.ImageTagConfiguration, trimVersionSignifier, hasSeparateTagPerReleaseBranch bool) (string, error) {
	var releaseImageUri string

	if imageTagConfiguration.ReleaseImageTagFormat != "" {
		releaseImageTagPrefix := generateFormattedTagPrefix(imageTagConfiguration.ReleaseImageTagFormat, tagOptions)
		releaseImageUri = fmt.Sprintf("%s/%s:%s-eks-a",
			r.ReleaseContainerRegistry,
			repoName,
			releaseImageTagPrefix,
		)
	} else {
		releaseImageUri = fmt.Sprintf("%s/%s:%s-eks-a",
			r.ReleaseContainerRegistry,
			repoName,
			tagOptions["gitTag"],
		)
	}

	var semver string
	if r.DevRelease {
		if r.Weekly {
			semver = r.DevReleaseUriVersion
		} else {
			currentSourceImageUri, _, err := GetSourceImageURI(r, name, repoName, tagOptions, imageTagConfiguration, trimVersionSignifier, hasSeparateTagPerReleaseBranch)
			if err != nil {
				return "", errors.Cause(err)
			}

			previousReleaseImageSemver, err := GetPreviousReleaseImageSemver(r, releaseImageUri)
			if err != nil {
				return "", errors.Cause(err)
			}
			if previousReleaseImageSemver == "" {
				semver = r.DevReleaseUriVersion
			} else {
				fmt.Printf("Previous release image semver for %s image: %s\n", repoName, previousReleaseImageSemver)
				previousReleaseImageUri := fmt.Sprintf("%s-%s", releaseImageUri, previousReleaseImageSemver)

				sameDigest, err := CompareHashWithPreviousBundle(r, currentSourceImageUri, previousReleaseImageUri)
				if err != nil {
					return "", errors.Cause(err)
				}
				if sameDigest {
					semver = previousReleaseImageSemver
					fmt.Printf("Image digest for %s image has not changed, tagging with previous dev release semver: %s\n", repoName, semver)
				} else {
					buildNumber, err := filereader.NewBuildNumberFromLastVersion(previousReleaseImageSemver, "vDev", r.BuildRepoBranchName)
					if err != nil {
						return "", err
					}
					newSemver, err := filereader.GetCurrentEksADevReleaseVersion("vDev", r, buildNumber)
					if err != nil {
						return "", err
					}
					semver = strings.ReplaceAll(newSemver, "+", "-")
					fmt.Printf("Image digest for %s image has changed, tagging with new dev release semver: %s\n", repoName, semver)
				}
			}
		}
	} else {
		semver = fmt.Sprintf("%d", r.BundleNumber)
	}

	releaseImageUri = fmt.Sprintf("%s-%s", releaseImageUri, semver)
	if trimVersionSignifier {
		releaseImageUri = strings.ReplaceAll(releaseImageUri, ":v", ":")
	}

	return releaseImageUri, nil
}

func generateFormattedTagPrefix(imageTagFormat string, tagOptions map[string]string) string {
	formattedTag := imageTagFormat
	re := regexp.MustCompile(`<(\w+)>`)
	searchResults := re.FindAllString(imageTagFormat, -1)
	for _, result := range searchResults {
		trimmedResult := strings.Trim(result, "<>")
		formattedTag = strings.ReplaceAll(formattedTag, result, tagOptions[trimmedResult])
	}
	return formattedTag
}

func CompareHashWithPreviousBundle(r *releasetypes.ReleaseConfig, currentSourceImageUri, previousReleaseImageUri string) (bool, error) {
	if r.DryRun {
		return false, nil
	}
	fmt.Printf("Comparing digests for [%s] and [%s]\n", currentSourceImageUri, previousReleaseImageUri)
	currentSourceImageUriDigest, err := ecr.GetImageDigest(currentSourceImageUri, r.SourceContainerRegistry, r.SourceClients.ECR.EcrClient)
	if err != nil {
		return false, errors.Cause(err)
	}

	previousReleaseImageUriDigest, err := ecrpublic.GetImageDigest(previousReleaseImageUri, r.ReleaseContainerRegistry, r.ReleaseClients.ECRPublic.Client)
	if err != nil {
		return false, errors.Cause(err)
	}

	return currentSourceImageUriDigest == previousReleaseImageUriDigest, nil
}

func GetPreviousReleaseImageSemver(r *releasetypes.ReleaseConfig, releaseImageUri string) (string, error) {
	var semver string
	if r.DryRun {
		semver = "v0.0.0-dev-build.0"
	} else {
		bundles := &anywherev1alpha1.Bundles{}
		bundleReleaseManifestKey := artifactutils.GetManifestFilepaths(r.DevRelease, r.Weekly, r.BundleNumber, constants.BundlesKind, r.BuildRepoBranchName, r.ReleaseDate)
		bundleManifestUrl := fmt.Sprintf("https://%s.s3.amazonaws.com/%s", r.ReleaseBucket, bundleReleaseManifestKey)
		if s3.KeyExists(r.ReleaseBucket, bundleReleaseManifestKey) {
			contents, err := filereader.ReadHttpFile(bundleManifestUrl)
			if err != nil {
				return "", fmt.Errorf("Error reading bundle manifest from S3: %v", err)
			}

			if err = yaml.Unmarshal(contents, bundles); err != nil {
				return "", fmt.Errorf("Error unmarshaling bundles manifest from [%s]: %v", bundleManifestUrl, err)
			}

			for _, versionedBundle := range bundles.Spec.VersionsBundles {
				vbImages := versionedBundle.Images()
				for _, image := range vbImages {
					if strings.Contains(image.URI, releaseImageUri) {
						imageUri := image.URI
						var differential int
						if r.BuildRepoBranchName == "main" {
							differential = 1
						} else {
							differential = 2
						}
						numDashes := strings.Count(imageUri, "-")
						splitIndex := numDashes - strings.Count(r.BuildRepoBranchName, "-") - differential
						imageUriSplit := strings.SplitAfterN(imageUri, "-", splitIndex)
						semver = imageUriSplit[len(imageUriSplit)-1]
					}
				}
			}
		}
	}
	return semver, nil
}