#!/usr/bin/env python3

import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed
from itertools import chain, groupby
import re
import sys

import boto3


SUPPORTED_ARCHS = {'arm64', 'x86_64'}

SRIOV_TYPES = (
    re.compile(r'^c3[a-z]*\.'),
    re.compile(r'^c4[a-z]*\.'),
    re.compile(r'^d2[a-z]*\.'),
    re.compile(r'^i2[a-z]*\.'),
    re.compile(r'^m4[a-z]*\.'),
    re.compile(r'^r3[a-z]*\.'),
)

IGNORE_TYPES = (
    re.compile(r'^dl\d+[a-z]*\.'),	# AI
    re.compile(r'^f\d+[a-z]*\.'),	# FPGA
    re.compile(r'^inf\d+[a-z]*\.'),	# Inferentia ASIC
    re.compile(r'^g\d+[a-z]*\.'),	# GPU
    re.compile(r'^h\d+[a-z]*\.'),	# Large local HDD
    re.compile(r'^mac(\d+|-)'),		# Apple Mac
    re.compile(r'^p\d+[a-z]*\.'),	# GPU
    re.compile(r'^trn\d+[a-z]*\.'),	# Trainium ASIC
    re.compile(r'^u-?\d+'),		# Ultra-high memory
    re.compile(r'^vt\d+[a-z]*\.'),	# Video transcoding
    re.compile(r'^x\d+[a-z]*\.'),	# High memory
    re.compile(r'^z\d+[a-z]*\.'),	# High CPU frequency
)


def get_all(paginator, attr, **kwargs):
    """Get all paginated results"""
    paged = chain.from_iterable(
        x.get(attr, [])
        for x in paginator.paginate(**kwargs)
    )
    return paged


def type_characteristics(typ):
    """Get relevant instance type characteristics"""
    m = re.match(r'^[a-z]+(\d+)', typ['InstanceType'])
    generation = m.group(1)
    baremetal = typ['BareMetal']
    hypervisor = typ.get('Hypervisor', '')
    ena = typ['NetworkInfo']['EnaSupport']
    sriov = (ena == 'unsupported' and
             any(x.match(typ['InstanceType']) for x in SRIOV_TYPES))
    manufacturer = typ['ProcessorInfo']['Manufacturer']
    archs = tuple(sorted(
        x for x in typ['ProcessorInfo']['SupportedArchitectures']
        if x in SUPPORTED_ARCHS
    ))
    bootmodes = tuple(sorted(typ['SupportedBootModes']))
    return (
        generation,
        baremetal,
        hypervisor,
        ena,
        sriov,
        manufacturer,
        archs,
        bootmodes,
    )


def find_types(region):
    """Find available instance types and key characteristics"""
    client = boto3.client('ec2', region_name=region)
    paginator = client.get_paginator('describe_instance_types')
    types = {
        typ['InstanceType']: type_characteristics(typ)
        for typ in get_all(paginator, 'InstanceTypes')
        if not any(x.match(typ['InstanceType']) for x in IGNORE_TYPES)
    }
    return types


# Parse command-line arguments
parser = argparse.ArgumentParser(
    description="Check AWS EC2 instance type coverage"
)
parser.add_argument('type', nargs='*', help="Instance type(s)")
args = parser.parse_args()

# Get list of all regions
regions = sorted(x['RegionName'] for x in
                 boto3.client('ec2').describe_regions()['Regions'])

# Identify available instance types across selected regions
with ThreadPoolExecutor(max_workers=len(regions)) as executor:
    futures = {executor.submit(find_types,
                               region=region): region
               for region in regions}
    types = {futures[future]: future.result()
             for future in as_completed(futures)}

# Group types by characteristics
groups = sorted(
    tuple(sorted(set(x[1] for x in v)))
    for k, v in groupby(sorted(
            (chars, typ)
            for region in regions
            for typ, chars in types[region].items()
    ), key=lambda x: x[0])
)

# Identify coverage for each group
covered = {
    g: tuple(x for x in args.type if x in g)
    for g in groups
}

# Identify unknown, missing, and redundant instance types
unknown = [x for x in args.type if not any(x in g for g in groups)]
missing = [k for k, v in covered.items() if not v]
redundant = [v for k, v in covered.items() if len(v) > 1]
if unknown:
    print("\n".join("Unknown: %s" % x for x in unknown))
if missing:
    print("\n".join("Missing: %s" % "/".join(x) for x in missing))
if redundant:
    print("\n".join("Redundant: %s" % "/".join(x) for x in redundant))
if unknown or missing or redundant:
    sys.exit(1)
