import {Directive, Inject, Injectable, OptionDefinition, uuid4} from '@extollo/lib'
import { IpRange } from "../../models/IpRange.model";
import { Setting } from "../../models/Setting.model";
import { Provisioner } from "../../services/Provisioner.service";
import { IpAddress, isIpAddress, isSubnet, Subnet } from "../../types";
import * as crypto from 'crypto'

@Injectable()
export class StandupDirective extends Directive {
    @Inject()
    protected readonly provisioner!: Provisioner

    getKeywords(): string | string[] {
        return 'standup'
    }

    getDescription(): string {
        return 'perform initial configuration and setup for the control server'
    }

    getOptions(): OptionDefinition[] {
        return [
            '--pve-master-node {name} | the name of the master node in the PVE cluster',
            '--pve-api-host {host} | the hostname or IP of a PVE cluster node',
            // '--pve-token-id {id} | the fully-qualified ID of a PVE API token',
            // '--pve-token-secret {secret} | secret for the PVE API token',
            '--pve-root-password {password} | password for the root@pam PVE user',
            '--ip-pool-start {start} | the starting address in the P5X IP pool',
            '--ip-pool-end {end} | the final address in the P5X IP pool',
            '--ip-pool-subnet {subnet} | the numeric subnet of the P5X IP pool (e.g. 24)',
            '--ip-pool-gateway {gateway} | the address of the gateway for the P5X IP pool',
            '--storage-pool {name} | the name of the PVE shared storage pool',
            '--dns-domain {name} | the DNS domain name used for K8s nodes',
            '--node-network-bridge {name} | the name of the PVE network bridge to which nodes will join',
            '--node-cpu-cores {cores} | the number of cores to provision each K8s node with',
            '--node-ram-mib {ram} | the amount of ram (in MiB) to provision each K8s node with',
        ]
    }

    public async handle(argv: string[]): Promise<void> {
        // Configure the settings:
        await Setting.set('pveMasterNode', `${this.getOptionOrFail('pve-master-node')}`)
        await Setting.set('pveApiHost', `${this.getOptionOrFail('pve-api-host')}`)
        // await Setting.set('pveTokenId', `${this.getOptionOrFail('pve-token-id')}`)
        // await Setting.set('pveTokenSecret', `${this.getOptionOrFail('pve-token-secret')}`)
        await Setting.set('pveRootPassword', `${this.getOptionOrFail('pve-root-password')}`)
        await Setting.set('pveStoragePool', `${this.getOptionOrFail('storage-pool')}`)
        await Setting.set('dnsDomain', `${this.getOptionOrFail('dns-domain')}`)
        await Setting.set('nodeNetworkBridge', `${this.getOptionOrFail('node-network-bridge')}`)
        await Setting.set('nodeCpus', this.getIntOptionOrFail('node-cpu-cores'))
        await Setting.set('nodeRamMib', this.getIntOptionOrFail('node-ram-mib'))
        await Setting.set('rootPassword', uuid4())

        const [sshPublicKey, sshPrivateKey] = await this.getSSHKeyPair()
        await Setting.set('sshPublicKey', sshPublicKey)
        await Setting.set('sshPrivateKey', sshPrivateKey)

        // Store the IP address pool:
        const ipRange = this.container().makeNew<IpRange>(IpRange)
        ipRange.name = 'Default address pool'
        ipRange.startIp = this.getIpOptionOrFail('ip-pool-start')
        ipRange.endIp = this.getIpOptionOrFail('ip-pool-end')
        ipRange.subnet = this.getSubnetOptionOrFail('ip-pool-subnet')
        ipRange.gatewayIp = this.getIpOptionOrFail('ip-pool-gateway')
        ipRange.save()

        // Provision the master node:
        const masterNode = await this.provisioner.provisionMasterNode()
    }

    protected getIntOptionOrFail(option: string): number {
        const val = parseInt(this.option(option, NaN), 10)
        if ( isNaN(val) ) {
            this.error(`Missing or malformed required integer option: --${option}`)
            process.exit(1)
        }
        return val
    }

    protected getSubnetOptionOrFail(option: string): Subnet {
        const val = parseInt(this.option(option, NaN), 10)
        if ( !isSubnet(val) ) {
            this.error(`Missing or malformed required numeric subnet: --${option}`)
            process.exit(1)
        }
        return val
    }

    protected getIpOptionOrFail(option: string): IpAddress {
        const val = `${this.option(option, '')}`
        if ( !isIpAddress(val) ) {
            this.error(`Missing or malformed required IP address: --${option}`)
            process.exit(1)
        }
        return val
    }

    protected getOptionOrFail(option: string): unknown {
        const val = this.option(option, undefined)
        if ( typeof val === 'undefined' ) {
            this.error(`Missing required option: --${option}`)
            process.exit(1)
        }
        return val
    }

    protected async getSSHKeyPair(): Promise<[string, string]> {
        return new Promise<[string, string]>((res, rej) => {
            crypto.generateKeyPair(
                'rsa',
                {
                    modulusLength: 4096,
                    publicKeyEncoding: {
                        type: 'spki',
                        format: 'pem',
                    },
                    privateKeyEncoding: {
                        type: 'pkcs8',
                        format: 'pem',
                        cipher: 'aes-256-cbc',
                        passphrase: '',
                    },
                },
                (err, pubkey, privkey) => {
                    if ( err ) {
                        return rej(err)
                    }

                    res([pubkey, privkey])
                },
            )
        })
    }

}
