"use strict";

interface DomainJson {
    name: string,
    id: string,
}

interface DomainServerJson {
    host: string,
    id: string,
    priority: number,
    weight: number,
    protocols: {
        https: number | undefined,
        wss: number | undefined,
        http: number | undefined,
        ws: number | undefined,
        udp: number | undefined
    }
}

interface EventJson {
    data: {
        event: {
            data: any
        }
    }
}

interface WellKnownJson {
    domain: DomainJson,
    domain_servers: DomainServerJson[]
}

export class Domain {
    name: string;
    id: string;
    servers: DomainServer[];
    invalidServers: DomainServer[];

    toString() {
        return `[${this.name}]`;
    }

    constructor(jsonData: DomainJson, domainServers: DomainServerJson[]) {
        // FIXME check values
        this.name = jsonData.name;
        this.id = jsonData.id;
        this.servers = [];
        for (const domainServer of domainServers) {
            this.servers.push(new DomainServer(domainServer));
        }
        this.invalidServers = [];
    }

    static async fromName(domainName: string): Promise<Domain> {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), 2000);

        const response = await fetch(`https://${domainName}/.well-known/usimp.json`, {
            redirect: "manual",
            signal: controller.signal,
        });

        clearTimeout(timer);
        if (!response.ok) {
            throw Error("Invalid response");
        }

        const data: WellKnownJson = await response.json();
        return new Domain(data.domain, data.domain_servers);
    }

    chooseDomainServer(): DomainServer {
        if (this.servers.length === 0) throw Error("No domain servers specified");

        const servers = this.servers.filter(srv => !this.invalidServers.map(srv => srv.id).includes(srv.id));
        if (servers.length === 0) throw Error("No domain servers reachable");
        if (servers.length === 1 && servers[0]) return servers[0];

        const priority = servers.reduce((min, srv) => Math.min(min, srv.priority), Infinity);
        const domainServers = servers.filter(srv => srv.priority === priority);
        if (domainServers.length === 1 && servers[0]) return servers[0];

        const totalWeight = domainServers.reduce((total, srv) => total + srv.weight, 0);
        const w = Math.floor(Math.random() * totalWeight);

        let accumulator = 0;
        for (const srv of domainServers) {
            accumulator += srv.weight;
            if (w < accumulator) return srv;
        }

        throw Error("Domain server selection did not work correctly");
    }
}

export class DomainServer {
    host: string;
    id: string;
    priority: number;
    weight: number;
    protocols: {
        https: number | undefined,
        wss: number | undefined,
        http: number | undefined,
        ws: number | undefined,
        udp: number | undefined
    };

    toString() {
        return `[${this.host}/${this.priority}/${this.weight}]`;
    }

    constructor(jsonData: DomainServerJson) {
        this.host = jsonData.host;
        this.id = jsonData.id;
        this.priority = jsonData.priority;
        this.weight = jsonData.weight;
        this.protocols = jsonData.protocols;
    }
}

export class Account {

}

export class Member {

}

export class Room {

}

export class Event {

}

export class Session {
    domain: Domain;
    server: DomainServer | null;
    token: string | null;

    httpBaseUrl: string | null;
    websocket: WebSocket | null;
    numRequests: number;

    constructor(domain: Domain) {
        this.domain = domain;
        this.numRequests = 0;
        this.server = null;
        this.token = null;
        this.httpBaseUrl = null;
        this.websocket = null;
    }

    async chooseDomainServer(): Promise<{ http: number | null, ws: number | null } | undefined> {
        while (!this.server) {
            this.server = this.domain.chooseDomainServer();

            const host = this.server.host;
            const protocols = this.server.protocols;

            /*
            if ("ws" in protocols) {
                this.websocket = new WebSocket(`ws://${host}:${protocols.ws}/_usimp/websocket`, ["usimp"]);
            }
            */

            // TODO http -> https
            if (protocols) {
                this.httpBaseUrl = `http://${host}:${protocols.http}/_usimp`;
                try {
                    return await this.ping();
                } catch {
                    this.domain.invalidServers.push(this.server);
                    this.server = null;
                }
            } else {
                console.warn(`Domain server ${this.server} does not support 'https' transport protocol`);
                this.domain.invalidServers.push(this.server);
                this.server = null;
            }
        }
        return undefined;
    }

    async send(endpoint: string, data: object, timeout: number = 2000, forceHttp: boolean = false) {
        this.numRequests++;

        if (!forceHttp && this.websocket) {
            this.websocket.send(JSON.stringify({
                'request_num': this.numRequests,
                'data': data
            }));
        } else {
            const controller = new AbortController();
            const timer = setTimeout(() => controller.abort(), timeout);

            let headers: Record<string, string> = {
                'Content-Type': 'application/json',
                'To-Domain': this.domain.id,
            };
            if (this.token) {
                headers['Authorization'] = `usimp ${this.token}`;
            }

            const startTime = new Date();
            const response = await fetch(`${this.httpBaseUrl}/${endpoint}`, {
                method: "POST",
                headers: headers,
                body: JSON.stringify(data),
                signal: controller.signal,
            });
            const endTime = new Date();
            clearTimeout(timer);

            const responseData = await response.json();
            responseData.duration = endTime.getUTCMilliseconds() - startTime.getUTCMilliseconds();
            return responseData;
        }
    }

    async ping() {
        let result = {"http": null, "ws": null};
        const resHttp = await this.send("ping", {}, undefined, true);
        result.http = resHttp.duration;

        if (this.websocket) {
            const resWs = await this.send("ping", {});
            result.ws = resWs.duration;
        }

        return result;
    }

    async authenticate(accountName: string, password: string) {
        const response = await this.send("authenticate", {
            type: "password",
            name: accountName,
            password: password,
        });
        if (response.status === "success") {
            this.token = response.data.token;
        }
        return response;
    }

    async sendEvent(roomId: string, data: object) {
        return this.send("send_event", {
            room_id: roomId,
            data: data,
        });
    }

    subscribe(func: (a: EventJson) => void) {
        this.numRequests++;
        if (this.websocket) {
            // TODO
        } else {
            let headers: Record<string, string> = {
                'Content-Type': 'application/json',
                'To-Domain': this.domain.id,
            };
            if (this.token) {
                headers['Authorization'] = `usimp ${this.token}`;
            }

            fetch(`${this.httpBaseUrl}/subscribe`, {
                method: "POST",
                headers: headers,
                body: JSON.stringify({}),
            }).then(response => {
                return response.json();
            }).then(response => {
                if (response.status === "success") {
                    this.subscribe(func);
                    func(response);
                } else {
                    setTimeout(() => {
                        this.subscribe(func);
                    }, 1000);
                }
            }).catch(() => {
                setTimeout(() => {
                    this.subscribe(func);
                }, 1000);
            });
        }
    }
}
