import {Injectable, Model, Field, FieldType, TypeTag, Maybe, collect, ErrorWithContext} from '@extollo/lib'
import * as IPCIDR from 'ip-cidr'
import {IpAddress, Subnet} from '../types'
import {Node} from './Node.model'

/**
 * IpRange Model
 * -----------------------------------
 * Represents an available IP address pool from which we can allocate node addresses.
 */
@Injectable()
export class IpRange extends Model<IpRange> {
    protected static table = 'p5x_ip_ranges'
    protected static key = 'id'

    public static async getDefault(): Promise<IpRange> {
        const ipRange = await this.query<IpRange>().first()
        if ( !ipRange ) {
            throw new Error('Unable to find default IP range!')
        }
        return ipRange
    }

    @Field(FieldType.serial)
    public id?: number

    @Field(FieldType.varchar)
    public name!: string

    @Field(FieldType.varchar, 'start_ip')
    public startIp!: IpAddress

    @Field(FieldType.varchar, 'end_ip')
    public endIp!: IpAddress

    @Field(FieldType.int)
    public subnet!: Subnet

    @Field(FieldType.varchar, 'gateway_ip')
    public gatewayIp!: IpAddress

    /** Get the next IP address in this range that has not already been allocated to a node. */
    public async getNextAvailable(): Promise<Maybe<IpAddress>> {
        // Generate the address pool
        const cidr = new IPCIDR(`${this.gatewayIp}/${this.subnet}`)
        const possibleIps = collect(cidr.toArray({ from: this.startIp, to: this.endIp }) as IpAddress[])
            .filterOut(x => x.endsWith('.0'))

        // Look up already-allocated addresses
        const allocatedIps = await Node.query<Node>().get().pluck('assignedIp')

        // Find the first available address
        return possibleIps.firstWhere(ip => !allocatedIps.includes(ip))
    }

    /** Like `getNextAvailable(...)`, but it will throw if the range is exhausted */
    public async getNextAvailableOrFail(): Promise<IpAddress> {
        const ip = await this.getNextAvailable()
        if ( !ip ) {
            throw new ErrorWithContext('Unable to get next available IP from range: is range exhausted?', {
                range: this.toObject(),
            })
        }
        return ip
    }
}
