import {Snowflake} from "discord-api-types/globals";
import {parse as uuidParse, v1 as uuidV1, validate as uuidValidate, version as uuidVersion} from "uuid";
import {SnowflakeUtil} from "discord.js";
import {fast1a52 as fnvFast1a52} from "fnv-plus";
import {UserTable} from "../database/users";

export class UsersManager {
    private readonly _table: UserTable

    constructor({users}: { users: UserTable }) {
        this._table = users
    }

    async isSnowflakeAdmin(snowflake: Snowflake): Promise<boolean> {
        return this._table.getUserExistsAndIsAdmin(userSnowflakeToUuid(snowflake))
    }

    async updateSnowflakeActivity(snowflake: Snowflake): Promise<void> {
        return this._table.createUserOrUpdateActiveTime(userSnowflakeToUuid(snowflake))
    }

    async setSnowflakeAdmin(snowflake: Snowflake): Promise<void> {
        return this._table.createOrSetUserAsAdmin(userSnowflakeToUuid(snowflake))
    }
}

export function userUuidToSnowflake(uuid: string): Snowflake | null {
    if (!uuidValidate(uuid) || uuidVersion(uuid) !== 1) {
        return null
    }
    const bytes = uuidParse(uuid)
    const time_low = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]
    const time_mid = bytes[4] << 8 | bytes[5]
    const time_high = (bytes[6] & 0x0F) << 8 | bytes[7]
    const timestamp = ((BigInt(time_high) << 48n) | (BigInt(time_mid) << 32n) | BigInt(time_low)) / 10000n
    const increment = BigInt((bytes[8] & 0x0F) << 8 | bytes[9])
    const workerId = BigInt((bytes[14] & 0x03) << 3 | (bytes[15] & 0x07) >> 5)
    const processId = BigInt(bytes[15] & 0x1F)

    return SnowflakeUtil.generate({
        timestamp,
        increment,
        workerId,
        processId,
    }).toString(10)
}

export function userSnowflakeToUuid(snowflake: Snowflake): string {
    const {
        timestamp,
        increment,
        workerId,
        processId,
    } = SnowflakeUtil.deconstruct(snowflake)

    // Grab 52 hash bits for filling the empty spaces in the UUID.
    const hashCode = fnvFast1a52(snowflake)
    // We get 10000 possibilities for number of 100-nanosecond periods.
    const nanoseconds = (hashCode % 10000)
    // The node ID is 48 bits, and we have 10 from the process and worker IDs.
    // The first bit must be 1 to indicate a made up node name, but the remaining 37 are open.
    // Grab 37 bits after removing the 10000 variants of the 100-nanosecond periods.
    const nodeIdPadding = Math.floor(hashCode / 10000) & 0x1FFFFFFFFF
    const nodeId = new Uint8Array(6)
    // Set the highest bit, indicating a multicast address...
    // ... indicating an invalid address, indicating a made up node name.
    // Then insert the hash padding and worker/process IDs.
    // It doesn't matter what order they're in, as long as it's consistent.
    // In this case, we choose the padding to be most significant, then worker, then process.
    nodeId[0] = 0x80 | ((nodeIdPadding >> 30) & 0x7F)
    nodeId[1] = (nodeIdPadding >> 22) & 0xFF
    nodeId[2] = (nodeIdPadding >> 14) & 0xFF
    nodeId[3] = (nodeIdPadding >> 6) & 0xFF
    nodeId[4] = ((nodeIdPadding & 0x3F) << 2) | Number((workerId >> 3n) & 0x03n)
    nodeId[5] = Number(((workerId & 0x07n) << 5n) | (processId & 0x1Fn))
    return uuidV1({
        rng: () => {
            throw Error("rng is not supposed to be invoked")
        },
        // The snowflake increment is 12 bits.
        // UUIDs use a 12 bit clock sequence number. Perfect fit!
        clockseq: Number(increment & 0xFFFn),
        nsecs: nanoseconds,
        msecs: Number(timestamp & 0x3FFFFFFFFFFn),
        node: nodeId,
    })
}