#!/bin/env zx
import fs from "fs"
import { exit } from "process";

const targetNumberOfCores = 32


const regions = JSON.parse(await $`aws ec2 describe-regions`).Regions.map(r => r.RegionName);

console.log("Available regions:", regions);


const instanceMap = JSON.parse(fs.readFileSync("instance-types.json", "utf-8"))
let pricings = JSON.parse(fs.readFileSync("spot-pricing.json", "utf-8"))

console.log("Spot pricing data:", pricings)

const burstableBaselines = {
    "t2.nano": 0.05,
    "t2.micro": 0.10,
    "t2.small": 0.20,
    "t2.medium": 0.20,
    "t2.large": 0.30,
    "t2.xlarge": 0.225,
    "t2.2xlarge": 0.17,
    "t3.nano": 0.05,
    "t3.micro": 0.10,
    "t3.small": 0.20,
    "t3.medium": 0.20,
    "t3.large": 0.30,
    "t3.xlarge": 0.40,
    "t3.2xlarge": 0.40,
    "t3a.nano": 0.05,
    "t3a.micro": 0.10,
    "t3a.small": 0.20,
    "t3a.medium": 0.20,
    "t3a.large": 0.30,
    "t3a.xlarge": 0.40,
    "t3a.2xlarge": 0.40,
    "t4g.nano": 0.05,
    "t4g.micro": 0.10,
    "t4g.small": 0.20,
    "t4g.medium": 0.20,
    "t4g.large": 0.30,
    "t4g.xlarge": 0.40,
    "t4g.2xlarge": 0.40
}

const performanceData = {
    "r7iz": 3077,
    "i7ie": 2989,
    "m7a": 2876,
    "c7a": 2870,
    "r7a": 2867,
    "c7i": 2856,
    "r7i": 2839,
    "c7i-flex": 2836,
    "m7i": 2831,
    "m7i-flex": 2761,
    "c6a": 2584,
    "m6a": 2584,
    "r6a": 2581,
    "m5zn": 2493,
    "m6in": 2288,
    "m6id": 2287,
    "r6i": 2286,
    "c6id": 2285,
    "r6idn": 2284,
    "m6i": 2284,
    "m6idn": 2283,
    "r6id": 2282,
    "i4i": 2280,
    "c6i": 2278,
    "c6in": 2276,
    "z1d": 2243,
    "r6in": 2243,
    "c5a": 2052,
    "c5ad": 2031,
    "c5n": 1895,
    "c5": 1894,
    "c5d": 1876,
    "t2": 1775,
    "r5n": 1735,
    "m5n": 1720,
    "r5dn": 1718,
    "m5dn": 1718,
    "m5": 1716,
    "r5": 1714,
    "r5b": 1712,
    "m5d": 1701,
    "r5d": 1679,
    "r4": 1574,
    "r3": 1572,
    "m4": 1542,
    "t3": 1517,
    "m5ad": 1405,
    "r5ad": 1404,
    "m5a": 1403,
    "r5a": 1394,
    "t3a": 1342
}


let sortedInstanceTypes = []

for (const p of pricings) {
    const i = instanceMap[p.InstanceType + "-" + p.AvailabilityZone.slice(0, p.AvailabilityZone.length - 1)]
    if (!i) continue
    if (i.VCpuInfo.DefaultVCpus > targetNumberOfCores) continue
    const sp = JSON.parse(JSON.stringify(i))
    sp.SpotPrice = p.SpotPrice
    sp.AvailabilityZone = p.AvailabilityZone
    let baseline = 1
    if (p.InstanceType in burstableBaselines) {
        baseline = burstableBaselines[p.InstanceType]
        continue
    }
    if (!p.AvailabilityZone.startsWith("eu-central")) {
        continue
    }
    const performance = (performanceData[p.InstanceType.split(".")[0]] ?? 1000) / 1000
    sp.SpotPricePerCore = p.SpotPrice / sp.VCpuInfo.DefaultVCpus / baseline / performance
    sortedInstanceTypes.push(sp)
}

sortedInstanceTypes = sortedInstanceTypes.sort((a, b) => a.SpotPricePerCore - b.SpotPricePerCore)

console.log("20 cheapest instance types:")
for (let i = 0; i < Math.min(20, sortedInstanceTypes.length); i++) {
    console.log(sortedInstanceTypes[i].InstanceType, sortedInstanceTypes[i].SpotPricePerCore, sortedInstanceTypes[i].SpotPrice, "in region", sortedInstanceTypes[i].AvailabilityZone)
}

const cheapestInstance = sortedInstanceTypes[0]
console.log("Cheapest instance type:", cheapestInstance)

const targetNumberOfInstances = Math.ceil(targetNumberOfCores / cheapestInstance.VCpuInfo.DefaultVCpus)
const targetRegion = cheapestInstance.AvailabilityZone.slice(0, cheapestInstance.AvailabilityZone.length - 1)

console.log(`Requesting ${targetNumberOfInstances} instances of type ${cheapestInstance.InstanceType} in region ${cheapestInstance.AvailabilityZone} at ${cheapestInstance.SpotPrice}$/h`)

//exit(0)

let vpc = JSON.parse(await $`aws ec2 describe-vpcs --filters Name=isDefault,Values=true --region ${targetRegion}`).Vpcs[0]

if (!vpc) {
    console.log("No default VPC found, creating one")
    await $`aws ec2 create-default-vpc --region ${targetRegion}`
    vpc = JSON.parse(await $`aws ec2 describe-vpcs --filters Name=isDefault,Values=true --region ${targetRegion}`).Vpcs[0]
}

if (!vpc.Ipv6CidrBlockAssociationSet) {
    console.log("No IPv6 CIDR block found, associating one")
    await $`aws ec2 associate-vpc-cidr-block --vpc-id ${vpc.VpcId} --ipv6-cidr-block-network-border-group ${targetRegion} --amazon-provided-ipv6-cidr-block --region ${targetRegion}`
    vpc = JSON.parse(await $`aws ec2 describe-vpcs --filters Name=isDefault,Values=true --region ${targetRegion}`).Vpcs[0]
}

let ipv6CidrBlock = vpc.Ipv6CidrBlockAssociationSet.filter(c => c.Ipv6CidrBlockState.State == "associated")[0].Ipv6CidrBlock

let subnets = JSON.parse(await $`aws ec2 describe-subnets --filters Name=vpc-id,Values=${vpc.VpcId} --region ${targetRegion}`).Subnets

let targetRegionSubnetId = subnets.filter(s => s.AvailabilityZone == cheapestInstance.AvailabilityZone)[0].SubnetId

for (const subnet of subnets) {
    if (subnet.Ipv6CidrBlockAssociationSet.length == 0) {
        const parts = ipv6CidrBlock.split("/")[0].split(":")
        let prefix = parts.slice(0, 3).join(":") + ":"
        let lastPart = parts[3].slice(0, 2)
        while (lastPart.length < 3) {
            lastPart = lastPart + "0"
        }
        prefix += lastPart
        let i = 0
        while (subnets.some(s => s.Ipv6CidrBlockAssociationSet.some(c => c.Ipv6CidrBlock.startsWith(`${prefix}${i}`)))) {
            i++
        }
        const cidrBlock = `${prefix}${i}::/64`


        console.log(`No IPv6 CIDR block found for subnet ${subnet.SubnetId}, associating one`)
        await $`aws ec2 associate-subnet-cidr-block --subnet-id ${subnet.SubnetId} --ipv6-cidr-block ${cidrBlock} --region ${targetRegion}`
        subnet.Ipv6CidrBlockAssociationSet = JSON.parse(await $`aws ec2 describe-subnets --filters Name=subnet-id,Values=${subnet.SubnetId} --region ${targetRegion}`).Subnets[0].Ipv6CidrBlockAssociationSet
    }
}

let securityGroup = JSON.parse(await $`aws ec2 describe-security-groups --filters Name=group-name,Values=default --region ${targetRegion}`).SecurityGroups[0]

if (!securityGroup.IpPermissions.some(p => p.UserIdGroupPairs.length == 0)) {
    console.log("Allowing all inbound traffic to default security group")
    if (securityGroup.IpPermissions.length > 0) {
        for (const p of securityGroup.IpPermissions.filter(p => p.UserIdGroupPairs.length > 0)) {
            await $`aws ec2 revoke-security-group-ingress --group-id ${securityGroup.GroupId} --ip-permissions IpProtocol=-1,UserIdGroupPairs=[{GroupId=${p.UserIdGroupPairs[0].GroupId}}] --region ${targetRegion}`
        }
    }
    await $`aws ec2 authorize-security-group-ingress --group-id ${securityGroup.GroupId} --ip-permissions IpProtocol=-1,FromPort=-1,ToPort=-1,Ipv6Ranges=[{CidrIpv6=::/0}] --region ${targetRegion}`
}

let internetGateways = JSON.parse(await $`aws ec2 describe-internet-gateways --region ${targetRegion}`).InternetGateways

if (internetGateways.length == 0) {
    console.log("No internet gateway found, creating one")
    await $`aws ec2 create-internet-gateway --region ${targetRegion}`
    internetGateways = JSON.parse(await $`aws ec2 describe-internet-gateways --region ${targetRegion}`).InternetGateways
}
if (!internetGateways.some(g => g.Attachments.some(a => a.VpcId == vpc.VpcId))) {
    console.log("Attaching internet gateway to VPC")
    await $`aws ec2 attach-internet-gateway --internet-gateway-id ${internetGateways[0].InternetGatewayId} --vpc-id ${vpc.VpcId} --region ${targetRegion}`
    internetGateways = JSON.parse(await $`aws ec2 describe-internet-gateways --region ${targetRegion}`).InternetGateways
}

let internetGateway = internetGateways.filter(g => g.Attachments.some(a => a.VpcId == vpc.VpcId))[0]

let routeTable = JSON.parse(await $`aws ec2 describe-route-tables --filters Name=vpc-id,Values=${vpc.VpcId} --region ${targetRegion}`).RouteTables[0]

if (routeTable.Routes.some(r => r.DestinationIpv6CidrBlock == "::/0" && r.GatewayId !== internetGateway.InternetGatewayId)) {
    console.log("Removing incorrect route to internet gateway")
    await $`aws ec2 delete-route --route-table-id ${routeTable.RouteTableId} --destination-ipv6-cidr-block ::/0 --region ${targetRegion}`
    routeTable = JSON.parse(await $`aws ec2 describe-route-tables --filters Name=vpc-id,Values=${vpc.VpcId} --region ${targetRegion}`).RouteTables[0]
}

if (!routeTable.Routes.some(r => r.DestinationIpv6CidrBlock == "::/0")) {
    console.log("Adding route to internet gateway")
    await $`aws ec2 create-route --route-table-id ${routeTable.RouteTableId} --destination-ipv6-cidr-block ::/0 --gateway-id ${internetGateway.InternetGatewayId} --region ${targetRegion}`
    routeTable = JSON.parse(await $`aws ec2 describe-route-tables --filters Name=vpc-id,Values=${vpc.VpcId} --region ${targetRegion}`).RouteTables[0]
}

const publiKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrJ2vQmLUH9Q5aJJHTAT3thfw2u/6GMukFf5m7PCGfTCvqjnO+Qjex/p2KmeBpPDoLzlhoE4ROLTT96A1bRg2S41MnrZEd+gTUpnpVjNVWbnZHvEpQjKAKYDLGOxfhEM9webeXwZd37rmodg6p5EYPCsphXK2IziKxI9WS1CekgsLxtW7JE3HrRzSSt7/Wzo1TBOzoGD/KoW4S9AozQiD0ZEgPwj9n7yjyjtlORKAsaJQwylpUYKOGvSNqRQykCsKrBnigOURmPDzwI65jzZj1SyYsBtcCrgfLvqFmJGUD0GdN4A/64Q8gF+xbh7HRI1SDzbXowbeHoYLSjQqQEa9Z peeters@enterprise.fritz.box"

let keyPairs = JSON.parse(await $`aws ec2 describe-key-pairs --filters Name=key-name,Values=enterprise --region ${targetRegion}`).KeyPairs
if (keyPairs.length == 0) {
    console.log("No key pair found, importing...")
    await $`aws ec2 import-key-pair --key-name enterprise --public-key-material ${publiKey} --region ${targetRegion}`
}

const image = JSON.parse(await $`aws ec2 describe-images --owners amazon --filters "Name=name,Values=ubuntu-minimal/images/hvm-ssd-gp3/ubuntu-oracular-24.10-amd64-minimal-20250321" --region ${targetRegion}`).Images[0]

const dateFrom = new Date(Math.ceil(new Date().getTime() / 1000) * 1000)
const dateUntil = new Date(Math.ceil(new Date(dateFrom.getTime() + 10 * 60 * 60 * 1000).getTime() / 1000) * 1000)

const setupScript = fs.readFileSync("setup.sh", "utf8")
const setupScriptBase64 = Buffer.from(setupScript).toString("base64")

/**
 * @type {import("@aws-sdk/client-ec2").CreateFleetRequest}
 */
const fleet = {
    TargetCapacitySpecification: {
        TotalTargetCapacity: targetNumberOfInstances,
        DefaultTargetCapacityType: "spot"
    },
    IamFleetRole: "arn:aws:iam::228068582184:role/aws-ec2-spot-fleet-tagging-role",
    SpotOptions: {
        AllocationStrategy: "priceCapacityOptimized",
        MaxTotalPrice: "0.2",
    },
    TargetCapacity: targetNumberOfInstances,
    ValidFrom: dateFrom.toISOString(),
    ValidUntil: dateUntil.toISOString(),
    Type: "request",
    TerminateInstancesWithExpiration: true,

    LaunchTemplateConfigs: subnets.map(s => {
        /**
         * @type {import("@aws-sdk/client-ec2").FleetLaunchTemplateConfigRequest}
         */
        const template = {
            LaunchTemplateSpecification: {
                ImageId: image.ImageId,
                BlockDeviceMappings: [
                    {
                        DeviceName: "/dev/sda1",
                        Ebs: {
                            DeleteOnTermination: true,
                            SnapshotId: image.BlockDeviceMappings.filter(b => !!b.Ebs)[0].Ebs.SnapshotId,
                            VolumeSize: 8,
                            VolumeType: "gp3",
                            Encrypted: false
                        }
                    }
                ],
                KeyName: "enterprise",
                UserData: setupScriptBase64,
                NetworkInterfaces: [
                    {
                        DeviceIndex: 0,
                        SubnetId: s.SubnetId,
                        DeleteOnTermination: true,
                        Groups: [],
                        AssociatePublicIpAddress: false,
                        Ipv6AddressCount: 1,
                    }
                ],
                InstanceType: cheapestInstance.InstanceType,
                Placement: {
                    AvailabilityZone: s.AvailabilityZone,
                    Tenancy: "default",
                },
            },
        }
        return template
    }),
}

console.log("Fleet configuration:", fleet)
//exit(0)

console.log("Requesting spot fleet...")
await $`aws ec2 request-spot-fleet --spot-fleet-request-config ${JSON.stringify(fleet)} --region ${targetRegion}`
