// Copyright 2021 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 updatemanifest implements the logic for the ssm agent s3 manifest.
package updatemanifest

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"strings"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/context"
	"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/versionutil"
)

func New(context context.T, info updateinfo.T, region string) T {
	return &manifestImpl{
		context:  context,
		info:     info,
		region:   region,
		manifest: &jsonManifest{},
	}
}

func (m *manifestImpl) LoadManifest(manifestPath string) error {
	var manifestBytes []byte
	var err error
	if manifestBytes, err = ioutil.ReadFile(manifestPath); err != nil {
		return err
	}

	err = json.Unmarshal(manifestBytes, m.manifest)

	if err != nil {
		return err
	}

	if err = m.validateManifest(appconfig.DefaultAgentName); err != nil {
		return err
	}

	return nil
}

// HasVersion returns if manifest file has particular version for package
func (m *manifestImpl) HasVersion(packageName string, version string) bool {
	log := m.context.Log()
	log.Debugf("checking if package %v has version %s", packageName, version)
	fileName := m.info.GenerateCompressedFileName(packageName)
	log.Debugf("Searching for file name %s", fileName)
	for _, p := range m.manifest.Packages {
		if p.Name == packageName {
			log.Debugf("Found package %s", packageName)
			for _, f := range p.Files {
				if f.Name == fileName {
					log.Debugf("Found file name %s", fileName)
					for _, v := range f.AvailableVersions {
						if v.Version == version || version == updateconstants.PipelineTestVersion {
							log.Debugf("Found version %s", version)
							return true
						}
					}

				}
			}
		}
	}
	log.Warnf("Did not find file name %s with version %s for package %s", fileName, version, packageName)
	return false
}

// GetLatestVersion returns latest version for specific package
func (m *manifestImpl) GetLatestVersion(packageName string) (result string, err error) {
	var version = updateconstants.MinimumVersion
	var compareResult = 0
	for _, p := range m.manifest.Packages {
		if p.Name == packageName {
			for _, f := range p.Files {
				if f.Name == m.info.GenerateCompressedFileName(packageName) {
					for _, v := range f.AvailableVersions {
						if compareResult, err = versionutil.VersionCompare(v.Version, version); err != nil {
							return version, err
						}
						if compareResult > 0 {
							version = v.Version
						}
					}
				}
			}
		}
	}
	if version == updateconstants.MinimumVersion {
		log := m.context.Log()
		log.Debugf("Filename: %v", m.info.GenerateCompressedFileName(packageName))
		log.Debugf("Package Name: %v", packageName)
		log.Debugf("Manifest: %v", m)
		return version, fmt.Errorf("cannot find the latest version for package %v", packageName)
	}

	return version, nil
}

// GetLatestActiveVersion returns latest active version for specific package
func (m *manifestImpl) GetLatestActiveVersion(packageName string) (result string, err error) {
	var version = updateconstants.MinimumVersion
	var compareResult = 0
	for _, p := range m.manifest.Packages {
		if p.Name == packageName {
			for _, f := range p.Files {
				if f.Name == m.info.GenerateCompressedFileName(packageName) {
					for _, v := range f.AvailableVersions {
						actualStatus, verErr := m.getVersionStatus(v)

						if verErr != nil {
							// This can only happen when agent doesn't understand the 3 supported statuses
							m.context.Log().Warnf("Failed to get version status: %v", verErr)
							continue
						}

						if actualStatus != VersionStatusActive {
							continue
						}

						if compareResult, err = versionutil.VersionCompare(v.Version, version); err != nil {
							return version, err
						}
						if compareResult > 0 {
							version = v.Version
						}
					}
				}
			}
		}
	}
	if version == updateconstants.MinimumVersion {
		log := m.context.Log()
		log.Debugf("Filename: %v", m.info.GenerateCompressedFileName(packageName))
		log.Debugf("Package Name: %v", packageName)
		log.Debugf("Manifest: %v", m)
		return version, fmt.Errorf("cannot find the latest version for package %v", packageName)
	}

	return version, nil
}

// GetDownloadURLAndHash returns download source url and hash value
func (m *manifestImpl) GetDownloadURLAndHash(
	packageName string,
	version string) (result string, hash string, err error) {
	fileName := m.info.GenerateCompressedFileName(packageName)
	var region string
	if m.region == "" {
		region, err = m.context.Identity().Region()
		if err != nil {
			return
		}
	} else {
		region = m.region
	}

	for _, p := range m.manifest.Packages {
		if p.Name == packageName {
			for _, f := range p.Files {
				if f.Name == fileName {
					for _, v := range f.AvailableVersions {
						if version == v.Version || version == updateconstants.PipelineTestVersion {
							result = m.manifest.URIFormat
							result = strings.Replace(result, updateconstants.RegionHolder, region, -1)
							result = strings.Replace(result, updateconstants.PackageNameHolder, packageName, -1)
							result = strings.Replace(result, updateconstants.PackageVersionHolder, version, -1)
							result = strings.Replace(result, updateconstants.FileNameHolder, f.Name, -1)
							if version == updateconstants.PipelineTestVersion {
								return result, "", nil
							}
							return result, v.Checksum, nil
						}
					}
				}
			}
		}
	}

	return "", "", fmt.Errorf("incorrect package name or version, %v, %v", packageName, version)
}

func (m *manifestImpl) getVersionStatus(version *packageVersion) (string, error) {
	switch version.Status {
	case "":
		return VersionStatusActive, nil
	case VersionStatusDeprecated, VersionStatusActive, VersionStatusInactive:
		return version.Status, nil
	default:
		return "", fmt.Errorf("invalid status %s for version %s", version.Status, version.Version)
	}
}

func (m *manifestImpl) isVersionStatus(packageName string, version string, status string) (isValid bool, err error) {
	log := m.context.Log()
	fileName := m.info.GenerateCompressedFileName(packageName)
	for _, p := range m.manifest.Packages {
		if p.Name == packageName {
			log.Infof("found package %v", packageName)
			for _, f := range p.Files {
				if f.Name == fileName {
					for _, v := range f.AvailableVersions {
						if v.Version == version {
							var actualStatus string
							actualStatus, err = m.getVersionStatus(v)

							if err != nil {
								return
							}

							log.Infof("Version %v status in manifest is %v while checking if status is %v", version, actualStatus, status)
							return actualStatus == status, nil
						}
					}
					break
				}
			}
		}
	}

	return isValid, fmt.Errorf("cannot find  %v information for %v in Manifest file", fileName, version)
}

func (m *manifestImpl) IsVersionDeprecated(
	packageName string,
	version string) (isValid bool, err error) {
	return m.isVersionStatus(packageName, version, VersionStatusDeprecated)
}

func (m *manifestImpl) IsVersionActive(
	packageName string,
	version string) (isValid bool, err error) {
	return m.isVersionStatus(packageName, version, VersionStatusActive)
}

// validateManifest makes sure all the fields are provided.
func (m *manifestImpl) validateManifest(packageName string) error {
	log := m.context.Log()
	if len(m.manifest.URIFormat) == 0 {
		return fmt.Errorf("uri format cannot be null in the Manifest file")
	}
	fileName := m.info.GenerateCompressedFileName(packageName)
	foundPackage := false
	foundFile := false
	for _, p := range m.manifest.Packages {
		if p.Name == packageName {
			log.Infof("found package %v", packageName)
			foundPackage = true
			for _, f := range p.Files {
				if f.Name == fileName {
					foundFile = true
					if len(f.AvailableVersions) == 0 {
						return fmt.Errorf("at least one available version is required for the %v", fileName)
					}

					log.Infof("found file %v", fileName)
					break
				}
			}
		}
	}

	if !foundPackage {
		return fmt.Errorf("cannot find the %v information in the Manifest file", packageName)
	}
	if !foundFile {
		return fmt.Errorf("cannot find the %v information in the Manifest file", fileName)
	}

	return nil
}
