// Copyright 2023 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. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 downloadmanager helps us with file download related functions in ssm-setup-cli
package downloadmanager

import (
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"time"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/backoffconfig"
	"github.com/aws/amazon-ssm-agent/agent/context"
	"github.com/aws/amazon-ssm-agent/agent/fileutil"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/network"
	"github.com/aws/amazon-ssm-agent/agent/setupcli/utility"
	"github.com/aws/amazon-ssm-agent/agent/updateutil/updateconstants"
	"github.com/aws/amazon-ssm-agent/agent/updateutil/updateinfo"
	"github.com/aws/amazon-ssm-agent/agent/updateutil/updatemanifest"
	"github.com/aws/amazon-ssm-agent/common/identity/endpoint"
	"github.com/cenkalti/backoff/v4"
)

const (
	manifestJsonFileName             = "ssm-agent-manifest.json"
	s3Service                        = "s3"
	s3RegularEndpointPrefix          = "https://s3"
	s3DualStackEndpointPrefix        = "https://s3.dualstack"
	lowerKernelVersionSupportedAgent = "3.0.1479.0"
	testVersion                      = "255.255.65535.999"
)

var (
	utilHttpDownload          = utility.HttpDownload
	updateInfoNew             = updateinfo.New
	updateManifestNew         = updatemanifest.New
	fileUtilUnCompress        = fileutil.Uncompress
	fileUtilityReadContent    = utility.HttpReadContent
	backOffRetry              = backoff.Retry
	computeAgentChecksumFunc  = utility.ComputeCheckSum
	hasLowerKernelVersionFunc = hasLowerKernelVersion
)

type downloadManager struct {
	log           log.T
	ctx           context.T
	bucketUrl     string
	region        string
	version       string
	manifestURL   string
	updateInfo    updateinfo.T
	manifestInfo  updatemanifest.T
	isNano        bool
	artifactsPath string
}

// New returns a new instance of DownloadManager
func New(log log.T, region string, manifestURL string, updateInfo updateinfo.T, setupCLIArtifactsPath string, isNano bool, useDualStackEndpoint bool) IDownloadManager {
	downloadMgrLog := log.WithContext("[DownloadManager]")
	var err error

	config := appconfig.DefaultConfig()

	// Apply UseDualStackEndpoint setting
	config.Agent.UseDualStackEndpoint = useDualStackEndpoint

	ctx := context.Default(downloadMgrLog, config, nil)
	if updateInfo == nil {
		updateInfo, err = updateinfo.New(ctx)
		if err != nil {
			downloadMgrLog.Errorf("Error while initiating update Info: %v", err)
			return nil
		}
	}

	endpointHelper := endpoint.NewEndpointHelper(log, config)
	if endpointHelper == nil {
		downloadMgrLog.Errorf("Error while initiating endpoint helper: %v", err)
		return nil
	}
	s3Endpoint := endpointHelper.GetServiceEndpoint(s3Service, region)
	downloadManagerRef := &downloadManager{
		log:           downloadMgrLog,
		ctx:           ctx,
		region:        region,
		updateInfo:    updateInfo,
		bucketUrl:     s3Endpoint,
		manifestURL:   manifestURL, // field is optional
		isNano:        isNano,
		artifactsPath: setupCLIArtifactsPath,
	}
	err = downloadManagerRef.Init()
	if err != nil {
		downloadMgrLog.Errorf("initialization failed: %v", err)
		return nil
	}
	return downloadManagerRef
}

func (d *downloadManager) Init() error {
	s3Url := d.getRegionManifestUrl()

	// downloads manifest based on the URL retrieved above and stores it in local path
	manifestFilePath, err := utilHttpDownload(d.log, s3Url, d.artifactsPath)
	if err != nil || manifestFilePath == "" {
		return fmt.Errorf("error while downloading manifest: %v", err)
	}

	updateManifestObj := updateManifestNew(d.ctx, d.updateInfo, d.region)
	d.manifestInfo = updateManifestObj

	err = updateManifestObj.LoadManifest(manifestFilePath)
	if err != nil {
		return fmt.Errorf("error while loading manifest: %v", err)
	}
	return nil
}

// DownloadArtifacts downloads agent artifacts from S3 bucket
func (d *downloadManager) DownloadArtifacts(installVersion string, manifestUrl string, artifactsStorePath string) error {
	var agentDownloadURL, agentHashInManifest string

	logger := d.log

	var err error
	// generate agent artifacts URL and checksum using the manifest loaded
	if agentDownloadURL, agentHashInManifest, err = d.manifestInfo.GetDownloadURLAndHash(appconfig.DefaultAgentName, installVersion); err != nil {
		return fmt.Errorf("error while getting target location and target hash: %v", err)
	}
	if d.ctx.AppConfig().Agent.UseDualStackEndpoint {
		agentDownloadURL = strings.Replace(agentDownloadURL, s3RegularEndpointPrefix, s3DualStackEndpointPrefix, 1)
	}

	generatedUrl := d.getS3BucketUrl() + "/"
	generatedUrl += appconfig.DefaultAgentName + "/" + installVersion + "/" + d.updateInfo.GenerateCompressedFileName(appconfig.DefaultAgentName)
	if generatedUrl != agentDownloadURL {
		d.log.Warnf("URL does not match %v %v", generatedUrl, agentDownloadURL)
	}

	// download agent artifacts using the generated URL before and store in local path
	agentSetupFilePath, err := utilHttpDownload(logger, generatedUrl, artifactsStorePath)
	if err != nil || agentSetupFilePath == "" {
		return fmt.Errorf("error while downloading agent artifacts file: %v", err)
	}

	// compute checksum of downloaded binary
	agentCheckSum, err := computeAgentChecksumFunc(agentSetupFilePath)
	if err != nil {
		return fmt.Errorf("failed to fetch checksum: %v", err)
	}

	// validate checksum using manifest
	if agentCheckSum != agentHashInManifest {
		return fmt.Errorf("checksum validation failed: %v", err)
	}

	// Un-compress downloaded files
	err = d.fileUnCompress(logger, agentSetupFilePath, artifactsStorePath)

	return err
}

// DownloadLatestSSMSetupCLI downloads latest SSM Setup CLI
func (d *downloadManager) DownloadLatestSSMSetupCLI(artifactsStorePath string, expectedSetupCLICheckSum string) error {
	logger := d.log

	logger.Info("Downloading SSM Setup CLI")
	folderName := d.updateInfo.GeneratePlatformBasedFolderName()

	// generate ssm-setup-cli s3 url
	ssmSetupCLIS3URL, err := d.generateLatestSSMSetupCLIS3Url(folderName)
	if err != nil {
		return fmt.Errorf("error while generating SSM Setup CLI URL: %v", err)
	}

	// Download ssm-setup CLI
	downloadedSSMSetupCLIFilePath, err := utilHttpDownload(logger, ssmSetupCLIS3URL, artifactsStorePath)
	if err != nil || downloadedSSMSetupCLIFilePath == "" {
		return fmt.Errorf("error while downloading SSM Setup CLI: %v", err)
	}

	// compute checksum of downloaded binary
	downloadedCLICheckSum, err := computeAgentChecksumFunc(downloadedSSMSetupCLIFilePath)
	if err != nil {
		return fmt.Errorf("failed to fetch checksum: %v", err)
	}

	if downloadedCLICheckSum != expectedSetupCLICheckSum {
		return fmt.Errorf("checksum mismatch with latest ssm-setup-cli. Please retry after downloading latest ssm-setup-cli")
	}

	logger.Infof("Downloaded SSM-Setup-CLI successfully")
	return nil
}

// GetStableVersion downloads the stable version file and returns the stable version number
func (d *downloadManager) GetStableVersion() (string, error) {
	stableVersionURL, err := d.getStableVersionURL()
	if err != nil {
		return "", fmt.Errorf("error while generating stable version URL %v", err)
	}
	stableVersion, err := d.readVersionFromURL(stableVersionURL)
	return stableVersion, err
}

// GetLatestVersion downloads the latest version file and returns the latest version number
func (d *downloadManager) GetLatestVersion() (string, error) {
	if hasLowerKernelVersionFunc() {
		return lowerKernelVersionSupportedAgent, nil
	}
	latestVersion, err := d.manifestInfo.GetLatestActiveVersion(appconfig.DefaultAgentName)
	if err != nil {
		return "", fmt.Errorf("error while getting the latest version from manifest: %v", err)
	}
	if latestVersion == testVersion {
		latestVersionURL, err := d.getLatestVersionURL()
		if err != nil {
			return "", fmt.Errorf("error while generating latest version URL %v", err)
		}
		latestVersion, err := d.readVersionFromURL(latestVersionURL)
		return latestVersion, err
	}
	return latestVersion, err
}

// getLatestVersionURL gets the latest version from URL
func (d *downloadManager) getLatestVersionURL() (string, error) {
	latestVersionURL := fmt.Sprintf("%s/%s/%s", d.getS3BucketUrl(), utility.LatestVersionString, utility.VersionFile)
	s3URL, err := url.Parse(latestVersionURL)
	if err != nil {
		return "", fmt.Errorf("error while parsing s3URL: %v", err)
	}
	return s3URL.String(), nil
}

func (d *downloadManager) readVersionFromURL(versionURL string) (string, error) {
	var err error
	d.log.Infof("Retrieving version from: %s", versionURL)
	exponentialBackOff, err := backoffconfig.GetDefaultExponentialBackoff()
	if err != nil {
		return "", fmt.Errorf("failed to initialize backoff module: %v", err)
	}

	var content string
	err = backOffRetry(func() error {
		httpTimeout := 30 * time.Second
		tr := network.GetDefaultTransport(d.log, appconfig.DefaultConfig())
		client := &http.Client{
			Transport: tr,
			Timeout:   httpTimeout,
		}
		// use http client to download
		contentBytes, readErr := fileUtilityReadContent(versionURL, client)
		if readErr != nil {
			return fmt.Errorf("failed to read response from %s: %v", versionURL, readErr)
		}
		if contentBytes == nil {
			return fmt.Errorf("response code is nil")
		}
		content = string(contentBytes)
		return nil
	}, exponentialBackOff)

	if err != nil {
		return "", fmt.Errorf("failed to get version from %s: %v", versionURL, err)
	}
	version := strings.TrimSpace(content)
	if !regexp.MustCompile(`^\d+.\d+.\d+.\d+$`).Match([]byte(version)) {
		return "", fmt.Errorf("invalid version format returned from %s: %s", versionURL, version)
	}

	d.log.Infof("Got version from %v: %s", versionURL, version)
	return version, nil
}

func (d *downloadManager) generateLatestSSMSetupCLIS3Url(folderName string) (string, error) {
	s3BucketUrl := d.getS3BucketUrl()
	ssmSetupCLIURL := fmt.Sprintf("%s/%s/%s/%s", s3BucketUrl, utility.LatestVersionString, folderName, utility.SSMSetupCLIBinary)
	s3URL, err := url.Parse(ssmSetupCLIURL)
	if err != nil {
		return "", fmt.Errorf("error while parsing s3URL")
	}
	return s3URL.String(), nil
}

// getS3BucketUrl returns s3 bucket URL
func (d *downloadManager) getS3BucketUrl() string {
	httpsPrefix := "https://"
	if d.manifestURL != "" {
		url := strings.TrimSpace(d.manifestURL)
		url = strings.TrimRight(url, updateconstants.ManifestFile)
		url = strings.TrimSuffix(url, "/")
		url = strings.TrimSuffix(url, "\\")
		return url
	}
	bucketName := strings.TrimSuffix(updateconstants.BucketPath, "/")
	return strings.Replace(httpsPrefix+d.bucketUrl+bucketName, updateconstants.RegionHolder, d.region, -1)
}

func (d *downloadManager) getStableVersionURL() (string, error) {
	s3BucketUrl := d.getS3BucketUrl()
	stableVersionURL := fmt.Sprintf("%s/%s/%s", s3BucketUrl, utility.StableVersionString, utility.VersionFile)
	s3URL, err := url.Parse(stableVersionURL)
	if err != nil {
		return "", fmt.Errorf("error while parsing stable version S3 URL: %v", err)
	}
	return s3URL.String(), nil
}

// getRegionManifestUrl gets region based manifest URL
func (d *downloadManager) getRegionManifestUrl() string {
	s3BucketUrl := d.getS3BucketUrl()
	s3URL, err := url.Parse(fmt.Sprintf("%s"+"/"+manifestJsonFileName, s3BucketUrl))
	if err != nil {
		return ""
	}
	return s3URL.String()
}
