//go:generate struct-markdown

package common

import (
	"fmt"
	"log"
	"regexp"

	"github.com/hashicorp/packer/packer-plugin-sdk/template/config"
	"github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate"
)

// AMIConfig is for common configuration related to creating AMIs.
type AMIConfig struct {
	// The name of the resulting AMI that will appear when managing AMIs in the
	// AWS console or via APIs. This must be unique. To help make this unique,
	// use a function like timestamp (see [template
	// engine](/docs/templates/engine) for more info).
	AMIName string `mapstructure:"ami_name" required:"true"`
	// The description to set for the resulting
	// AMI(s). By default this description is empty.  This is a
	// [template engine](/docs/templates/engine), see [Build template
	// data](#build-template-data) for more information.
	AMIDescription string `mapstructure:"ami_description" required:"false"`
	// The type of virtualization for the AMI
	// you are building. This option is required to register HVM images. Can be
	// paravirtual (default) or hvm.
	AMIVirtType string `mapstructure:"ami_virtualization_type" required:"false"`
	// A list of account IDs that have access to
	// launch the resulting AMI(s). By default no additional users other than the
	// user creating the AMI has permissions to launch it.
	AMIUsers []string `mapstructure:"ami_users" required:"false"`
	// A list of groups that have access to
	// launch the resulting AMI(s). By default no groups have permission to launch
	// the AMI. all will make the AMI publicly accessible.
	AMIGroups []string `mapstructure:"ami_groups" required:"false"`
	// A list of product codes to
	// associate with the AMI. By default no product codes are associated with the
	// AMI.
	AMIProductCodes []string `mapstructure:"ami_product_codes" required:"false"`
	// A list of regions to copy the AMI to.
	// Tags and attributes are copied along with the AMI. AMI copying takes time
	// depending on the size of the AMI, but will generally take many minutes.
	AMIRegions []string `mapstructure:"ami_regions" required:"false"`
	// Set to true if you want to skip
	// validation of the ami_regions configuration option. Default false.
	AMISkipRegionValidation bool `mapstructure:"skip_region_validation" required:"false"`
	// Key/value pair tags applied to the AMI. This is a [template
	// engine](/docs/templates/engine), see [Build template
	// data](#build-template-data) for more information.
	AMITags map[string]string `mapstructure:"tags" required:"false"`
	// Same as [`tags`](#tags) but defined as a singular repeatable block
	// containing a `key` and a `value` field. In HCL2 mode the
	// [`dynamic_block`](/docs/configuration/from-1.5/expressions#dynamic-blocks)
	// will allow you to create those programatically.
	AMITag config.KeyValues `mapstructure:"tag" required:"false"`
	// Enable enhanced networking (ENA but not SriovNetSupport) on
	// HVM-compatible AMIs. If set, add `ec2:ModifyInstanceAttribute` to your
	// AWS IAM policy.
	//
	// Note: you must make sure enhanced networking is enabled on your
	// instance. See [Amazon's documentation on enabling enhanced
	// networking](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking.html#enabling_enhanced_networking).
	AMIENASupport config.Trilean `mapstructure:"ena_support" required:"false"`
	// Enable enhanced networking (SriovNetSupport but not ENA) on
	// HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` to your
	// AWS IAM policy. Note: you must make sure enhanced networking is enabled
	// on your instance. See [Amazon's documentation on enabling enhanced
	// networking](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking.html#enabling_enhanced_networking).
	// Default `false`.
	AMISriovNetSupport bool `mapstructure:"sriov_support" required:"false"`
	// Force Packer to first deregister an existing
	// AMI if one with the same name already exists. Default false.
	AMIForceDeregister bool `mapstructure:"force_deregister" required:"false"`
	// Force Packer to delete snapshots
	// associated with AMIs, which have been deregistered by force_deregister.
	// Default false.
	AMIForceDeleteSnapshot bool `mapstructure:"force_delete_snapshot" required:"false"`
	// Whether or not to encrypt the resulting AMI when
	// copying a provisioned instance to an AMI. By default, Packer will keep
	// the encryption setting to what it was in the source image. Setting false
	// will result in an unencrypted image, and true will result in an encrypted
	// one.
	//
	// If you have used the `launch_block_device_mappings` to set an encryption
	// key and that key is the same as the one you want the image encrypted with
	// at the end, then you don't need to set this field; leaving it empty will
	// prevent an unnecessary extra copy step and save you some time.
	//
	// Please note that if you are using an account with the global "Always
	// encrypt new EBS volumes" option set to `true`, Packer will be unable to
	// override this setting, and the final image will be encryoted whether
	// you set this value or not.
	AMIEncryptBootVolume config.Trilean `mapstructure:"encrypt_boot" required:"false"`
	// ID, alias or ARN of the KMS key to use for AMI encryption. This
	// only applies to the main `region` -- any regions the AMI gets copied to
	// copied will be encrypted by the default EBS KMS key for that region,
	// unless you set region-specific keys in AMIRegionKMSKeyIDs.
	//
	// Set this value if you select `encrypt_boot`, but don't want to use the
	// region's default KMS key.
	//
	// If you have a custom kms key you'd like to apply to the launch volume,
	// and are only building in one region, it is more efficient to leave this
	// and `encrypt_boot` empty and to instead set the key id in the
	// launch_block_device_mappings (you can find an example below). This saves
	// potentially many minutes at the end of the build by preventing Packer
	// from having to copy and re-encrypt the image at the end of the build.
	//
	// For valid formats see *KmsKeyId* in the [AWS API docs -
	// CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html).
	// This field is validated by Packer, when using an alias, you will have to
	// prefix `kms_key_id` with `alias/`.
	AMIKmsKeyId string `mapstructure:"kms_key_id" required:"false"`
	// regions to copy the ami to, along with the custom kms key id (alias or
	// arn) to use for encryption for that region. Keys must match the regions
	// provided in `ami_regions`. If you just want to encrypt using a default
	// ID, you can stick with `kms_key_id` and `ami_regions`. If you want a
	// region to be encrypted with that region's default key ID, you can use an
	// empty string `""` instead of a key id in this map. (e.g. `"us-east-1":
	// ""`) However, you cannot use default key IDs if you are using this in
	// conjunction with `snapshot_users` -- in that situation you must use
	// custom keys. For valid formats see *KmsKeyId* in the [AWS API docs -
	// CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html).
	//
	// This option supercedes the `kms_key_id` option -- if you set both, and
	// they are different, Packer will respect the value in
	// `region_kms_key_ids` for your build region and silently disregard the
	// value provided in `kms_key_id`.
	AMIRegionKMSKeyIDs map[string]string `mapstructure:"region_kms_key_ids" required:"false"`
	// If true, Packer will not check whether an AMI with the `ami_name` exists
	// in the region it is building in. It will use an intermediary AMI name,
	// which it will not convert to an AMI in the build region. It will copy
	// the intermediary AMI into any regions provided in `ami_regions`, then
	// delete the intermediary AMI. Default `false`.
	AMISkipBuildRegion bool `mapstructure:"skip_save_build_region"`
	// Key/value pair tags to apply to snapshot. They will override AMI tags if
	// already applied to snapshot. This is a [template
	// engine](/docs/templates/engine), see [Build template
	// data](#build-template-data) for more information.
	SnapshotTags map[string]string `mapstructure:"snapshot_tags" required:"false"`
	// Same as [`snapshot_tags`](#snapshot_tags) but defined as a singular
	// repeatable block containing a `key` and a `value` field. In HCL2 mode the
	// [`dynamic_block`](/docs/configuration/from-1.5/expressions#dynamic-blocks)
	// will allow you to create those programatically.
	SnapshotTag config.KeyValues `mapstructure:"snapshot_tag" required:"false"`
	// A list of account IDs that have
	// access to create volumes from the snapshot(s). By default no additional
	// users other than the user creating the AMI has permissions to create
	// volumes from the backing snapshot(s).
	SnapshotUsers []string `mapstructure:"snapshot_users" required:"false"`
	// A list of groups that have access to
	// create volumes from the snapshot(s). By default no groups have permission
	// to create volumes from the snapshot(s). all will make the snapshot
	// publicly accessible.
	SnapshotGroups []string `mapstructure:"snapshot_groups" required:"false"`
}

func stringInSlice(s []string, searchstr string) bool {
	for _, item := range s {
		if item == searchstr {
			return true
		}
	}
	return false
}

func (c *AMIConfig) Prepare(accessConfig *AccessConfig, ctx *interpolate.Context) []error {
	var errs []error

	errs = append(errs, c.SnapshotTag.CopyOn(&c.SnapshotTags)...)
	errs = append(errs, c.AMITag.CopyOn(&c.AMITags)...)

	if c.AMIName == "" {
		errs = append(errs, fmt.Errorf("ami_name must be specified"))
	}

	// Make sure that if we have region_kms_key_ids defined,
	//  the regions in region_kms_key_ids are also in ami_regions
	if len(c.AMIRegionKMSKeyIDs) > 0 {
		for kmsKeyRegion := range c.AMIRegionKMSKeyIDs {
			if !stringInSlice(c.AMIRegions, kmsKeyRegion) {
				errs = append(errs, fmt.Errorf("Region %s is in region_kms_key_ids but not in ami_regions", kmsKeyRegion))
			}
		}
	}

	errs = append(errs, c.prepareRegions(accessConfig)...)

	// Prevent sharing of default KMS key encrypted volumes with other aws users
	if len(c.AMIUsers) > 0 {
		if len(c.AMIKmsKeyId) == 0 && c.AMIEncryptBootVolume.True() {
			errs = append(errs, fmt.Errorf("Cannot share AMI encrypted with default KMS key"))
		}
		if len(c.AMIRegionKMSKeyIDs) > 0 {
			for _, kmsKey := range c.AMIRegionKMSKeyIDs {
				if len(kmsKey) == 0 {
					errs = append(errs, fmt.Errorf("Cannot share AMI encrypted with default KMS key for other regions"))
				}
			}
		}
	}

	kmsKeys := make([]string, 0)
	if len(c.AMIKmsKeyId) > 0 {
		kmsKeys = append(kmsKeys, c.AMIKmsKeyId)
	}
	if len(c.AMIRegionKMSKeyIDs) > 0 {
		for _, kmsKey := range c.AMIRegionKMSKeyIDs {
			if len(kmsKey) > 0 {
				kmsKeys = append(kmsKeys, kmsKey)
			}
		}
	}

	if len(kmsKeys) > 0 && !c.AMIEncryptBootVolume.True() {
		errs = append(errs, fmt.Errorf("If you have set either "+
			"region_kms_key_ids or kms_key_id, encrypt_boot must also be true."))

	}
	for _, kmsKey := range kmsKeys {
		if !ValidateKmsKey(kmsKey) {
			errs = append(errs, fmt.Errorf("%q is not a valid KMS Key Id.", kmsKey))
		}
	}

	if len(c.SnapshotUsers) > 0 {
		if len(c.AMIKmsKeyId) == 0 && len(c.AMIRegionKMSKeyIDs) == 0 && c.AMIEncryptBootVolume.True() {
			errs = append(errs, fmt.Errorf("Cannot share snapshot encrypted "+
				"with default KMS key, see https://www.packer.io/docs/builders/amazon-ebs#region_kms_key_ids for more information"))
		}
		if len(c.AMIRegionKMSKeyIDs) > 0 {
			for _, kmsKey := range c.AMIRegionKMSKeyIDs {
				if len(kmsKey) == 0 {
					errs = append(errs, fmt.Errorf("Cannot share snapshot encrypted with default KMS key"))
				}
			}
		}
	}

	if len(c.AMIName) < 3 || len(c.AMIName) > 128 {
		errs = append(errs, fmt.Errorf("ami_name must be between 3 and 128 characters long"))
	}

	if c.AMIName != templateCleanAMIName(c.AMIName) {
		errs = append(errs, fmt.Errorf("AMIName should only contain "+
			"alphanumeric characters, parentheses (()), square brackets ([]), spaces "+
			"( ), periods (.), slashes (/), dashes (-), single quotes ('), at-signs "+
			"(@), or underscores(_). You can use the `clean_resource_name` template "+
			"filter to automatically clean your ami name."))
	}

	if len(errs) > 0 {
		return errs
	}

	return nil
}

func (c *AMIConfig) prepareRegions(accessConfig *AccessConfig) (errs []error) {
	if len(c.AMIRegions) > 0 {
		regionSet := make(map[string]struct{})
		regions := make([]string, 0, len(c.AMIRegions))

		for _, region := range c.AMIRegions {
			// If we already saw the region, then don't look again
			if _, ok := regionSet[region]; ok {
				continue
			}

			// Mark that we saw the region
			regionSet[region] = struct{}{}

			// Make sure that if we have region_kms_key_ids defined,
			// the regions in ami_regions are also in region_kms_key_ids
			if len(c.AMIRegionKMSKeyIDs) > 0 {
				if _, ok := c.AMIRegionKMSKeyIDs[region]; !ok {
					errs = append(errs, fmt.Errorf("Region %s is in ami_regions but not in region_kms_key_ids", region))
				}
			}
			if (accessConfig != nil) && (region == accessConfig.RawRegion) {
				// make sure we don't try to copy to the region we originally
				// create the AMI in.
				log.Printf("Cannot copy AMI to AWS session region '%s', deleting it from `ami_regions`.", region)
				continue
			}
			regions = append(regions, region)
		}

		c.AMIRegions = regions
	}
	return errs
}

// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html
func ValidateKmsKey(kmsKey string) (valid bool) {
	kmsKeyIdPattern := `[a-f0-9-]+$`
	aliasPattern := `alias/[a-zA-Z0-9:/_-]+$`
	kmsArnStartPattern := `^arn:aws(-us-gov)?:kms:([a-z]{2}-(gov-)?[a-z]+-\d{1})?:(\d{12}):`
	if regexp.MustCompile(fmt.Sprintf("^%s", kmsKeyIdPattern)).MatchString(kmsKey) {
		return true
	}
	if regexp.MustCompile(fmt.Sprintf("^%s", aliasPattern)).MatchString(kmsKey) {
		return true
	}
	if regexp.MustCompile(fmt.Sprintf("%skey/%s", kmsArnStartPattern, kmsKeyIdPattern)).MatchString(kmsKey) {
		return true
	}
	if regexp.MustCompile(fmt.Sprintf("%s%s", kmsArnStartPattern, aliasPattern)).MatchString(kmsKey) {
		return true
	}
	return false
}
