import { GuildMember } from "discord.js";
import { getRepository, In } from "typeorm";
import { KnownUser } from "@shared/db/entity/KnownUser";
import { HTTPError } from "got/dist/source";
import humanizeDuration from "humanize-duration";

const VALID_EXTENSIONS = new Set([
    "png",
    "jpg",
    "jpeg",
    "bmp",
]);

export type EsModuleClass<T> = { prototype: T; new(...params: unknown[]): T; };
export function isModuleClass<T>(obj: unknown) : obj is EsModuleClass<T> {
    return Object.prototype.hasOwnProperty.call(obj, "prototype");
}

export function isDevelopment(): boolean {
    return process.env.NODE_ENV == "dev";
}

export function isValidImage(fileName?: string): boolean {
    if (!fileName)
        return false;
    const extPosition = fileName.lastIndexOf(".");
    if (extPosition < 0)
        return false;
    const ext = fileName.substring(extPosition + 1).toLowerCase();
    return VALID_EXTENSIONS.has(ext);
}

export async function isAuthorisedAsync(member: GuildMember | null | undefined): Promise<boolean> {
    if (!member)
        return false;

    const repo = getRepository(KnownUser);

    const user = await repo.findOne({
        where: { userID: member.id },
        select: ["canModerate"]
    });

    if (user && user.canModerate)
        return true;

    const role = await repo.findOne({
        select: ["userID"],
        where: {
            userID: In(member.roles.cache.keyArray()),
            canModerate: true
        }
    });
    if (role)
        return true;
    return false;
}

export function compareNumbers<T>(prop: (o: T) => number) {
    return (a: T, b: T): -1 | 0 | 1 => {
        const ap = prop(a);
        const bp = prop(b);

        if (ap < bp)
            return 1;
        else if (ap > bp)
            return -1;
        return 0;
    };
}

export type Dict<TVal> = { [key: string]: TVal };

export function getNumberEnums<E>(e : Record<keyof E, number>) : number[] {
    return Object.keys(e).filter(k => typeof e[k as keyof E] === "number").map(k => e[k as keyof E]);
}

export function formatString(str: string, vars: Record<string, string>): string {
    return Object.keys(vars).filter(s => Object.prototype.hasOwnProperty.call(vars, s)).reduce((s, cur) => s.replace(`{${cur}}`, vars[cur]), str);
}

export async function tryDo<TResult>(promise: Promise<TResult>) : Promise<Option<{result: TResult}, {error: unknown}>> {
    try {
        return {ok: true, result: await promise};
    } catch(err) {
        return {ok: false, error: err};
    }
}

export async function assertOk<T>(promise: Promise<T>): Promise<T> {
    try {
        return await promise;
    } catch (err) {
        if (hasStackTrace(err)) {
            const trace: {stack?: string} = {};
            Error.captureStackTrace(trace);
            err.stack = `${err.stack}\nCaused by: ${trace.stack}`;
        }
        throw err;
    }
}

export function isHttpError(err?: unknown): err is HTTPError {
    return err && Object.prototype.hasOwnProperty.call(err, "response");
}

export function hasStackTrace(reason?: unknown): reason is {stack: unknown} {
    return reason && Object.prototype.hasOwnProperty.call(reason, "stack");
}

export function parseArgs(str: string): string[] {
    const result: string[] = [];

    let quoteMode = false;
    let escapeNext = false;
    let buffer = "";
    for (const char of str) {
        if (!escapeNext)
            if (char == "\"") {
                if (!quoteMode) {
                    quoteMode = true;
                } else {
                    quoteMode = false;
                    result.push(buffer);
                    buffer = "";
                }
                continue;
            } else if (char == "\\") {
                escapeNext = true;
                continue;
            } else if (/\s/.test(char)) {
                if (buffer.length == 0)
                    continue;
                if (!quoteMode) {
                    result.push(buffer);
                    buffer = "";
                    continue;
                }
            }
        buffer += char;
        escapeNext = false;
    }
    if (buffer.length != 0)
        result.push(buffer);

    return result;
}

export const UNIT_MEASURES: humanizeDuration.UnitMeasuresOptions = {
    ms: 1,
    s: 1000,
    m: 60 * 1000,
    h: 60 * 60 * 1000,
    d: 24 * 60 * 60 * 1000,
    w: 7 * 24 * 60 * 60 * 1000,
    mo: 30 * 24 * 60 * 60 * 1000,
    y: 365 * 24 * 60 * 60 * 1000
};
const durationUnits = UNIT_MEASURES as Record<string, number>;
const durationPattern = new RegExp(`(\\d+(?:[.,]?\\d+)?)\\s*(${Object.keys(durationUnits).sort((a, b) => b.length - a.length).join("|")})`, "g");

export function parseDuration(s: string): number | undefined {
    if (!s)
        return undefined;
    s = s.trim();
    let buffer = s;
    let result = 0;
    let match;
    while ((match = durationPattern.exec(s))) {
        buffer = buffer.replace(match[0], "").trim();
        result += +match[1] * durationUnits[match[2]];
    }
    if (buffer.length != 0)
        return undefined;
    return result;
}

export type Ok<T> = T & { ok: true };
export type Error<T> = T & { ok: false };
export type Option<T, U = unknown> = Ok<T> | Error<U>;