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

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/spf13/cobra"
	"go.uber.org/multierr"

	commandline "github.com/aws/amazon-ec2-instance-selector/v3/pkg/cli"
	"github.com/aws/amazon-ec2-instance-selector/v3/pkg/env"
	"github.com/aws/amazon-ec2-instance-selector/v3/pkg/instancetypes"
	"github.com/aws/amazon-ec2-instance-selector/v3/pkg/selector"
	"github.com/aws/amazon-ec2-instance-selector/v3/pkg/selector/outputs"
	"github.com/aws/amazon-ec2-instance-selector/v3/pkg/sorter"
)

const (
	binName             = "ec2-instance-selector"
	awsRegionEnvVar     = "AWS_REGION"
	defaultRegionEnvVar = "AWS_DEFAULT_REGION"
	defaultProfile      = "default"
	awsConfigFile       = "~/.aws/config"
	// 0 means the last price
	// increasing this results in a lot more API calls to EC2 which can slow things down.
	spotPricingDaysBack = 0

	tableOutput     = "table"
	tableWideOutput = "table-wide"
	oneLine         = "one-line"
	bubbleTeaOutput = "interactive"

	// Sort filter default.
	instanceNamePath = ".InstanceType"
)

// Filter Flag Constants.
const (
	vcpus                            = "vcpus"
	memory                           = "memory"
	vcpusToMemoryRatio               = "vcpus-to-memory-ratio"
	cpuArchitecture                  = "cpu-architecture"
	cpuManufacturer                  = "cpu-manufacturer"
	gpus                             = "gpus"
	gpuMemoryTotal                   = "gpu-memory-total"
	gpuManufacturer                  = "gpu-manufacturer"
	gpuModel                         = "gpu-model"
	inferenceAccelerators            = "inference-accelerators"
	inferenceAcceleratorManufacturer = "inference-accelerator-manufacturer"
	inferenceAcceleratorModel        = "inference-accelerator-model"
	placementGroupStrategy           = "placement-group-strategy"
	usageClass                       = "usage-class"
	rootDeviceType                   = "root-device-type"
	enaSupport                       = "ena-support"
	efaSupport                       = "efa-support"
	hibernationSupport               = "hibernation-support"
	baremetal                        = "baremetal"
	fpgaSupport                      = "fpga-support"
	burstSupport                     = "burst-support"
	hypervisor                       = "hypervisor"
	availabilityZones                = "availability-zones"
	currentGeneration                = "current-generation"
	networkInterfaces                = "network-interfaces"
	networkPerformance               = "network-performance"
	networkEncryption                = "network-encryption"
	ipv6                             = "ipv6"
	allowList                        = "allow-list"
	denyList                         = "deny-list"
	virtualizationType               = "virtualization-type"
	pricePerHour                     = "price-per-hour"
	instanceStorage                  = "instance-storage"
	diskType                         = "disk-type"
	diskEncryption                   = "disk-encryption"
	nvme                             = "nvme"
	ebsOptimized                     = "ebs-optimized"
	ebsOptimizedBaselineBandwidth    = "ebs-optimized-baseline-bandwidth"
	ebsOptimizedBaselineThroughput   = "ebs-optimized-baseline-throughput"
	ebsOptimizedBaselineIOPS         = "ebs-optimized-baseline-iops"
	freeTier                         = "free-tier"
	autoRecovery                     = "auto-recovery"
	dedicatedHosts                   = "dedicated-hosts"
	debug                            = "debug"
	generation                       = "generation"
	instanceTypes                    = "instance-types"
)

// Aggregate Filter Flags.
const (
	instanceTypeBase = "base-instance-type"
	flexible         = "flexible"
	service          = "service"
)

// Configuration Flag Constants.
const (
	maxResults    = "max-results"
	profile       = "profile"
	help          = "help"
	verbose       = "verbose"
	version       = "version"
	region        = "region"
	output        = "output"
	cacheTTL      = "cache-ttl"
	cacheDir      = "cache-dir"
	sortDirection = "sort-direction"
	sortBy        = "sort-by"
)

// versionID is overridden at compilation with the version based on the git tag
var versionID = "dev"

func main() {
	log.SetOutput(os.Stderr)
	log.SetPrefix("NOTE: ")
	log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))

	shortUsage := "A tool to filter EC2 Instance Types based on various resource criteria"
	longUsage := binName + ` is a CLI tool to filter EC2 instance types based on resource criteria. 
Filtering allows you to select all the instance types that match your application requirements.
Full docs can be found at github.com/aws/amazon-` + binName
	examples := fmt.Sprintf(`%s --vcpus 4 --region us-east-2 --availability-zones us-east-2b
%s --memory-min 4 --memory-max 8 --vcpus-min 4 --vcpus-max 8 --region us-east-2`, binName, binName)

	runFunc := func(cmd *cobra.Command, args []string) {}
	cli := commandline.New(binName, shortUsage, longUsage, examples, runFunc)

	cliOutputTypes := []string{
		tableOutput,
		tableWideOutput,
		oneLine,
		bubbleTeaOutput,
	}
	resultsOutputFn := outputs.SimpleInstanceTypeOutput

	cliSortDirections := []string{
		sorter.SortAscending,
		sorter.SortAsc,
		sorter.SortDescending,
		sorter.SortDesc,
	}

	// Registers flags with specific input types from the cli pkg
	// Filter Flags - These will be grouped at the top of the help flags

	cli.Int32MinMaxRangeFlags(vcpus, cli.StringMe("c"), nil, "Number of vcpus available to the instance type.")
	cli.ByteQuantityMinMaxRangeFlags(memory, cli.StringMe("m"), nil, "Amount of Memory available (Example: 4 GiB)")
	cli.RatioFlag(vcpusToMemoryRatio, nil, nil, "The ratio of vcpus to GiBs of memory. (Example: 1:2)")
	cli.StringOptionsFlag(cpuArchitecture, cli.StringMe("a"), nil, "CPU architecture [x86_64, amd64, x86_64_mac, i386, arm64, or arm64_mac]", []string{"x86_64", "x86_64_mac", "amd64", "i386", "arm64", "arm64_mac"})
	cli.StringOptionsFlag(cpuManufacturer, nil, nil, "CPU manufacturer [amd, intel, aws, apple]", []string{"amd", "intel", "aws", "apple"})
	cli.Int32MinMaxRangeFlags(gpus, cli.StringMe("g"), nil, "Total Number of GPUs (Example: 4)")
	cli.ByteQuantityMinMaxRangeFlags(gpuMemoryTotal, nil, nil, "Number of GPUs' total memory (Example: 4 GiB)")
	cli.StringFlag(gpuManufacturer, nil, nil, "GPU Manufacturer name (Example: NVIDIA)", nil)
	cli.StringFlag(gpuModel, nil, nil, "GPU Model name (Example: K520)", nil)
	cli.IntMinMaxRangeFlags(inferenceAccelerators, nil, nil, "Total Number of inference accelerators (Example: 4)")
	cli.StringFlag(inferenceAcceleratorManufacturer, nil, nil, "Inference Accelerator Manufacturer name (Example: AWS)", nil)
	cli.StringFlag(inferenceAcceleratorModel, nil, nil, "Inference Accelerator Model name (Example: Inferentia)", nil)
	cli.StringOptionsFlag(placementGroupStrategy, nil, nil, "Placement group strategy: [cluster, partition, spread]", []string{"cluster", "partition", "spread"})
	cli.StringOptionsFlag(usageClass, cli.StringMe("u"), nil, "Usage class: [spot or on-demand]", []string{"spot", "on-demand"})
	cli.StringOptionsFlag(rootDeviceType, nil, nil, "Supported root device types: [ebs or instance-store]", []string{"ebs", "instance-store"})
	cli.BoolFlag(enaSupport, cli.StringMe("e"), nil, "Instance types where ENA is supported or required")
	cli.BoolFlag(efaSupport, nil, nil, "Instance types that support Elastic Fabric Adapters (EFA)")
	cli.BoolFlag(hibernationSupport, nil, nil, "Hibernation supported")
	cli.BoolFlag(baremetal, nil, nil, "Bare Metal instance types (.metal instances)")
	cli.BoolFlag(fpgaSupport, cli.StringMe("f"), nil, "FPGA instance types")
	cli.BoolFlag(burstSupport, cli.StringMe("b"), nil, "Burstable instance types")
	cli.StringOptionsFlag(hypervisor, nil, nil, "Hypervisor: [xen or nitro]", []string{"xen", "nitro"})
	cli.StringSliceFlag(availabilityZones, cli.StringMe("z"), nil, "Availability zones or zone ids to check EC2 capacity offered in specific AZs")
	cli.BoolFlag(currentGeneration, nil, nil, "Current generation instance types (explicitly set this to false to not return current generation instance types)")
	cli.Int32MinMaxRangeFlags(networkInterfaces, nil, nil, "Number of network interfaces (ENIs) that can be attached to the instance")
	cli.IntMinMaxRangeFlags(networkPerformance, nil, nil, "Bandwidth in Gib/s of network performance (Example: 100)")
	cli.BoolFlag(networkEncryption, nil, nil, "Instance Types that support automatic network encryption in-transit")
	cli.BoolFlag(ipv6, nil, nil, "Instance Types that support IPv6")
	cli.RegexFlag(allowList, nil, nil, "List of allowed instance types to select from w/ regex syntax (Example: m[3-5]\\.*)")
	cli.RegexFlag(denyList, nil, nil, "List of instance types which should be excluded w/ regex syntax (Example: m[1-2]\\.*)")
	cli.StringOptionsFlag(virtualizationType, nil, nil, "Virtualization Type supported: [hvm or pv]", []string{"hvm", "paravirtual", "pv"})
	cli.Float64MinMaxRangeFlags(pricePerHour, nil, nil, "Price/hour in USD (Example: 0.09)")
	cli.ByteQuantityMinMaxRangeFlags(instanceStorage, nil, nil, "Amount of local instance storage (Example: 4 GiB)")
	cli.StringOptionsFlag(diskType, nil, nil, "Disk Type: [hdd or ssd]", []string{"hdd", "ssd"})
	cli.BoolFlag(nvme, nil, nil, "EBS or local instance storage where NVME is supported or required")
	cli.BoolFlag(diskEncryption, nil, nil, "EBS or local instance storage where encryption is supported or required")
	cli.BoolFlag(ebsOptimized, nil, nil, "EBS Optimized is supported or default")
	cli.ByteQuantityMinMaxRangeFlags(ebsOptimizedBaselineBandwidth, nil, nil, "EBS Optimized baseline bandwidth (Example: 4 GiB)")
	cli.ByteQuantityMinMaxRangeFlags(ebsOptimizedBaselineThroughput, nil, nil, "EBS Optimized baseline throughput per second (Example: 4 GiB)")
	cli.IntMinMaxRangeFlags(ebsOptimizedBaselineIOPS, nil, nil, "EBS Optimized baseline IOPS per second (Example: 10000)")
	cli.BoolFlag(freeTier, nil, nil, "Free Tier supported")
	cli.BoolFlag(autoRecovery, nil, nil, "EC2 Auto-Recovery supported")
	cli.BoolFlag(dedicatedHosts, nil, nil, "Dedicated Hosts supported")
	cli.IntMinMaxRangeFlags(generation, nil, nil, "Generation of the instance type (i.e. c7i.xlarge is 7)")
	cli.StringSliceFlag(instanceTypes, nil, nil, "Instance Type names (must be exact, use allow-list for regex)")

	// Suite Flags - higher level aggregate filters that return opinionated result

	cli.SuiteStringFlag(instanceTypeBase, nil, nil, "Instance Type used to retrieve similarly spec'd instance types", nil)
	cli.SuiteBoolFlag(flexible, nil, nil, "Retrieves a group of instance types spanning multiple generations based on opinionated defaults and user overridden resource filters")
	cli.SuiteStringFlag(service, nil, nil, "Filter instance types based on service support (Example: emr-5.20.0)", nil)

	// Configuration Flags - These will be grouped at the bottom of the help flags

	cli.ConfigIntFlag(maxResults, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_MAX_RESULTS", 20), "The maximum number of instance types that match your criteria to return")
	cli.ConfigStringFlag(profile, nil, nil, "AWS CLI profile to use for credentials and config", nil)
	cli.ConfigStringFlag(region, cli.StringMe("r"), nil, "AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence)", nil)
	cli.ConfigStringFlag(output, cli.StringMe("o"), nil, fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil)
	cli.ConfigIntFlag(cacheTTL, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_CACHE_TTL", 0), "Cache TTLs in hours for pricing and instance type caches. Setting the cache to 0 will turn off caching and cleanup any on-disk caches.")
	cli.ConfigPathFlag(cacheDir, nil, env.WithDefaultString("EC2_INSTANCE_SELECTOR_CACHE_DIR", "~/.ec2-instance-selector/"), "Directory to save the pricing and instance type caches")
	cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs")
	cli.ConfigBoolFlag("debug", nil, nil, "Debug - prints debug log messages")
	cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help")
	cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version")
	cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(sorter.SortAscending), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections)
	cli.ConfigStringFlag(sortBy, nil, cli.StringMe(instanceNamePath), "Specify the field to sort by. Quantity flags present in this CLI (memory, gpus, etc.) or a JSON path to the appropriate instance type field (Ex: \".MemoryInfo.SizeInMiB\") is acceptable.", nil)

	// Parses the user input with the registered flags and runs type specific validation on the user input
	flags, err := cli.ParseAndValidateFlags()
	if err != nil {
		log.Printf("There was an error while parsing the commandline flags: %v", err)
		os.Exit(1)
	}

	if flags[help] != nil {
		os.Exit(0)
	}

	if flags[version] != nil {
		fmt.Printf("%s", versionID)
		os.Exit(0)
	}

	if flags[service] != nil {
		log.Println("--service eks is deprecated. EKS generally supports all instance types")
	}

	ctx := context.Background()
	cfg, err := config.LoadDefaultConfig(ctx,
		config.WithSharedConfigProfile(
			aws.ToString(
				cli.StringMe(flags[profile]),
			),
		),
		config.WithRegion(
			aws.ToString(
				cli.StringMe(flags[region]),
			),
		),
	)
	if err != nil {
		fmt.Printf("Failed to load default AWS configuration: %s\n", err.Error())
		os.Exit(1)
	}

	flags[region] = cfg.Region

	cacheTTLDuration := time.Hour * time.Duration(*cli.IntMe(flags[cacheTTL]))
	instanceSelector, err := selector.NewWithCache(ctx, cfg, cacheTTLDuration, *cli.StringMe(flags[cacheDir]))
	if err != nil {
		fmt.Printf("An error occurred when initializing the ec2 selector: %v", err)
		os.Exit(1)
	}
	if flags[debug] != nil {
		debugLogger := log.New(os.Stdout, time.Now().UTC().Format(time.RFC3339)+" DEBUG ", 0)
		instanceSelector.SetLogger(debugLogger)
	}
	shutdown := func() {
		if err := instanceSelector.Save(); err != nil {
			log.Printf("There was an error saving pricing caches: %v", err)
		}
	}
	registerShutdown(shutdown)

	sortField := cli.StringMe(flags[sortBy])
	lowercaseSortField := strings.ToLower(*sortField)
	outputFlag := cli.StringMe(flags[output])
	if outputFlag != nil && (*outputFlag == tableWideOutput || *outputFlag == bubbleTeaOutput) {
		// If output type is `table-wide`, simply print both prices for better comparison,
		//   even if the actual filter is applied on any one of those based on usage class
		// Save time by hydrating all caches in parallel
		if err := hydrateCaches(ctx, *instanceSelector); err != nil {
			log.Printf("%v", err)
		}
	} else {
		// Else, if price filters are applied, only hydrate the respective cache as we don't have to print the prices
		if flags[pricePerHour] != nil {
			if flags[usageClass] == nil || *cli.StringMe(flags[usageClass]) == "on-demand" {
				if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 {
					if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(ctx); err != nil {
						log.Printf("There was a problem refreshing the on-demand pricing cache: %v", err)
					}
				}
			} else {
				if instanceSelector.EC2Pricing.SpotCacheCount() == 0 {
					if err := instanceSelector.EC2Pricing.RefreshSpotCache(ctx, spotPricingDaysBack); err != nil {
						log.Printf("There was a problem refreshing the spot pricing cache: %v", err)
					}
				}
			}
		}

		// refresh appropriate caches if sorting by either spot or on demand pricing
		if strings.Contains(lowercaseSortField, "price") {
			if strings.Contains(lowercaseSortField, "spot") {
				if instanceSelector.EC2Pricing.SpotCacheCount() == 0 {
					if err := instanceSelector.EC2Pricing.RefreshSpotCache(ctx, spotPricingDaysBack); err != nil {
						log.Printf("There was a problem refreshing the spot pricing cache: %v", err)
					}
				}
			} else {
				if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 {
					if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(ctx); err != nil {
						log.Printf("There was a problem refreshing the on-demand pricing cache: %v", err)
					}
				}
			}
		}
	}

	var cpuArchitectureFilterValue *ec2types.ArchitectureType

	if arch, ok := flags[cpuArchitecture].(*string); ok && arch != nil {
		value := ec2types.ArchitectureType(*arch)
		cpuArchitectureFilterValue = &value
	}

	var cpuManufacturerFilterValue *selector.CPUManufacturer

	if cpuMan, ok := flags[cpuManufacturer].(*string); ok && cpuMan != nil {
		value := selector.CPUManufacturer(*cpuMan)
		cpuManufacturerFilterValue = &value
	}

	var virtualizationTypeFilterValue *ec2types.VirtualizationType

	if virtType, ok := flags[virtualizationType].(*string); ok && virtType != nil {
		value := ec2types.VirtualizationType(*virtType)
		virtualizationTypeFilterValue = &value
	}

	var deviceTypeFilterValue *ec2types.RootDeviceType

	if rootDev, ok := flags[rootDeviceType].(*string); ok && rootDev != nil {
		value := ec2types.RootDeviceType(*rootDev)
		deviceTypeFilterValue = &value
	}

	var usageClassFilterValue *ec2types.UsageClassType

	if useClass, ok := flags[usageClass].(*string); ok && useClass != nil {
		value := ec2types.UsageClassType(*useClass)
		usageClassFilterValue = &value
	}

	var hypervisorFilterValue *ec2types.InstanceTypeHypervisor

	if hype, ok := flags[hypervisor].(*string); ok && hype != nil {
		value := ec2types.InstanceTypeHypervisor(*hype)
		hypervisorFilterValue = &value
	}

	filters := selector.Filters{
		VCpusRange:                       cli.Int32RangeMe(flags[vcpus]),
		MemoryRange:                      cli.ByteQuantityRangeMe(flags[memory]),
		VCpusToMemoryRatio:               cli.Float64Me(flags[vcpusToMemoryRatio]),
		CPUArchitecture:                  cpuArchitectureFilterValue,
		CPUManufacturer:                  cpuManufacturerFilterValue,
		GpusRange:                        cli.Int32RangeMe(flags[gpus]),
		GpuMemoryRange:                   cli.ByteQuantityRangeMe(flags[gpuMemoryTotal]),
		GPUManufacturer:                  cli.StringMe(flags[gpuManufacturer]),
		GPUModel:                         cli.StringMe(flags[gpuModel]),
		InferenceAcceleratorsRange:       cli.IntRangeMe(flags[inferenceAccelerators]),
		InferenceAcceleratorManufacturer: cli.StringMe(flags[inferenceAcceleratorManufacturer]),
		InferenceAcceleratorModel:        cli.StringMe(flags[inferenceAcceleratorModel]),
		PlacementGroupStrategy:           cli.StringMe(flags[placementGroupStrategy]),
		UsageClass:                       usageClassFilterValue,
		RootDeviceType:                   deviceTypeFilterValue,
		EnaSupport:                       cli.BoolMe(flags[enaSupport]),
		EfaSupport:                       cli.BoolMe(flags[efaSupport]),
		HibernationSupported:             cli.BoolMe(flags[hibernationSupport]),
		Hypervisor:                       hypervisorFilterValue,
		BareMetal:                        cli.BoolMe(flags[baremetal]),
		Fpga:                             cli.BoolMe(flags[fpgaSupport]),
		Burstable:                        cli.BoolMe(flags[burstSupport]),
		Region:                           cli.StringMe(flags[region]),
		AvailabilityZones:                cli.StringSliceMe(flags[availabilityZones]),
		CurrentGeneration:                cli.BoolMe(flags[currentGeneration]),
		MaxResults:                       cli.IntMe(flags[maxResults]),
		NetworkInterfaces:                cli.Int32RangeMe(flags[networkInterfaces]),
		NetworkPerformance:               cli.IntRangeMe(flags[networkPerformance]),
		NetworkEncryption:                cli.BoolMe(flags[networkEncryption]),
		IPv6:                             cli.BoolMe(flags[ipv6]),
		AllowList:                        cli.RegexMe(flags[allowList]),
		DenyList:                         cli.RegexMe(flags[denyList]),
		InstanceTypeBase:                 cli.StringMe(flags[instanceTypeBase]),
		Flexible:                         cli.BoolMe(flags[flexible]),
		Service:                          cli.StringMe(flags[service]),
		VirtualizationType:               virtualizationTypeFilterValue,
		PricePerHour:                     cli.Float64RangeMe(flags[pricePerHour]),
		InstanceStorageRange:             cli.ByteQuantityRangeMe(flags[instanceStorage]),
		DiskType:                         cli.StringMe(flags[diskType]),
		DiskEncryption:                   cli.BoolMe(flags[diskEncryption]),
		NVME:                             cli.BoolMe(flags[nvme]),
		EBSOptimized:                     cli.BoolMe(flags[ebsOptimized]),
		EBSOptimizedBaselineBandwidth:    cli.ByteQuantityRangeMe(flags[ebsOptimizedBaselineBandwidth]),
		EBSOptimizedBaselineThroughput:   cli.ByteQuantityRangeMe(flags[ebsOptimizedBaselineThroughput]),
		EBSOptimizedBaselineIOPS:         cli.IntRangeMe(flags[ebsOptimizedBaselineIOPS]),
		FreeTier:                         cli.BoolMe(flags[freeTier]),
		AutoRecovery:                     cli.BoolMe(flags[autoRecovery]),
		DedicatedHosts:                   cli.BoolMe(flags[dedicatedHosts]),
		Generation:                       cli.IntRangeMe(flags[generation]),
		InstanceTypes:                    cli.StringSliceMe(flags[instanceTypes]),
	}

	if flags[verbose] != nil {
		resultsOutputFn = outputs.VerboseInstanceTypeOutput
		transformedFilters, err := instanceSelector.AggregateFilterTransform(ctx, filters)
		if err != nil {
			fmt.Printf("An error occurred while transforming the aggregate filters")
			os.Exit(1)
		}
		filtersJSON, err := filters.MarshalIndent("", "    ")
		if err != nil {
			fmt.Printf("An error occurred when printing filters due to --verbose being specified: %v", err)
			os.Exit(1)
		}
		transformedFiltersJSON, err := transformedFilters.MarshalIndent("", "    ")
		if err != nil {
			fmt.Printf("An error occurred when printing aggregate filters due to --verbose being specified: %v", err)
			os.Exit(1)
		}
		log.Println("\n\n\"Filters\":", string(filtersJSON))
		if string(transformedFiltersJSON) != string(filtersJSON) {
			log.Println("\n\n\"Transformed Filters\":", string(transformedFiltersJSON))
		} else {
			log.Println("There were no transformations on the filters to display")
		}
	}

	// fetch instance types without truncating results
	prevMaxResults := filters.MaxResults
	filters.MaxResults = nil
	instanceTypesDetails, err := instanceSelector.FilterVerbose(ctx, filters)
	if err != nil {
		fmt.Printf("An error occurred when filtering instance types: %v", err)
		os.Exit(1)
	}

	// sort instance types
	sortDirection := cli.StringMe(flags[sortDirection])
	instanceTypesDetails, err = sorter.Sort(instanceTypesDetails, *sortField, *sortDirection)
	if err != nil {
		fmt.Printf("Sorting error: %v", err)
		os.Exit(1)
	}

	// handle output format
	var itemsTruncated int
	var instanceTypes []string
	if outputFlag != nil && *outputFlag == bubbleTeaOutput {
		p := tea.NewProgram(outputs.NewBubbleTeaModel(instanceTypesDetails), tea.WithMouseCellMotion())
		if _, err := p.Run(); err != nil {
			fmt.Printf("An error occurred when starting bubble tea: %v", err)
			os.Exit(1)
		}

		shutdown()
		return
	} else {
		// handle regular output modes

		// truncate instance types based on user passed in maxResults
		instanceTypesDetails, itemsTruncated = truncateResults(prevMaxResults, instanceTypesDetails)
		if len(instanceTypesDetails) == 0 {
			log.Println("The criteria was too narrow and returned no valid instance types. Consider broadening your criteria so that more instance types are returned.")
			os.Exit(1)
		}

		// format instance types for output
		outputFn := getOutputFn(outputFlag, selector.InstanceTypesOutputFn(resultsOutputFn))
		instanceTypes = outputFn(instanceTypesDetails)
	}

	for _, instanceType := range instanceTypes {
		fmt.Println(instanceType)
	}

	if itemsTruncated > 0 {
		log.Printf("%d entries were truncated, increase --%s to see more", itemsTruncated, maxResults)
	}
	shutdown()
}

func hydrateCaches(ctx context.Context, instanceSelector selector.Selector) (errs error) {
	wg := &sync.WaitGroup{}
	hydrateTasks := []func(*sync.WaitGroup) error{
		func(waitGroup *sync.WaitGroup) error {
			defer waitGroup.Done()
			if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 {
				if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(ctx); err != nil {
					return multierr.Append(errs, fmt.Errorf("there was a problem refreshing the on-demand pricing cache: %w", err))
				}
			}
			return nil
		},
		func(waitGroup *sync.WaitGroup) error {
			defer waitGroup.Done()
			if instanceSelector.EC2Pricing.SpotCacheCount() == 0 {
				if err := instanceSelector.EC2Pricing.RefreshSpotCache(ctx, spotPricingDaysBack); err != nil {
					return multierr.Append(errs, fmt.Errorf("there was a problem refreshing the spot pricing cache: %w", err))
				}
			}
			return nil
		},
		func(waitGroup *sync.WaitGroup) error {
			defer waitGroup.Done()
			if instanceSelector.InstanceTypesProvider.CacheCount() == 0 {
				if _, err := instanceSelector.InstanceTypesProvider.Get(ctx, nil); err != nil {
					return multierr.Append(errs, fmt.Errorf("there was a problem refreshing the instance types cache: %w", err))
				}
			}
			return nil
		},
	}
	wg.Add(len(hydrateTasks))
	for _, task := range hydrateTasks {
		go func() {
			if err := task(wg); err != nil {
				log.Printf("Hydrate task error: %v", err)
			}
		}()
	}
	wg.Wait()
	return errs
}

func getOutputFn(outputFlag *string, currentFn selector.InstanceTypesOutputFn) selector.InstanceTypesOutputFn {
	outputFn := selector.InstanceTypesOutputFn(currentFn)
	if outputFlag != nil {
		switch *outputFlag {
		case tableWideOutput:
			return selector.InstanceTypesOutputFn(outputs.TableOutputWide)
		case tableOutput:
			return selector.InstanceTypesOutputFn(outputs.TableOutputShort)
		case oneLine:
			return selector.InstanceTypesOutputFn(outputs.OneLineOutput)
		}
	}
	return outputFn
}

func registerShutdown(shutdown func()) {
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigs
		shutdown()
	}()
}

func truncateResults(maxResults *int, instanceTypeInfoSlice []*instancetypes.Details) ([]*instancetypes.Details, int) {
	if maxResults == nil {
		return instanceTypeInfoSlice, 0
	}
	upperIndex := *maxResults
	if *maxResults > len(instanceTypeInfoSlice) {
		upperIndex = len(instanceTypeInfoSlice)
	}
	return instanceTypeInfoSlice[0:upperIndex], len(instanceTypeInfoSlice) - upperIndex
}
