import { IrcBridgeConfiguration } from "../integrations/Bridge";
import { Cache, CACHE_IRC_BRIDGE } from "../MemoryCache";
import IrcBridgeRecord from "../db/models/IrcBridgeRecord";
import Upstream from "../db/models/Upstream";
import UserScalarToken from "../db/models/UserScalarToken";
import { LogService } from "matrix-bot-sdk";
import * as request from "request";
import { ListLinksResponseItem, ListOpsResponse, QueryNetworksResponse } from "./models/irc";
import IrcBridgeNetwork from "../db/models/IrcBridgeNetwork";
import { ModularIrcResponse } from "../models/ModularResponses";

interface CachedNetwork {
    ircBridgeId: number;
    bridgeNetworkId: string;
    bridgeUserId: string;
    displayName: string;
    domain: string;
    isEnabled: boolean;
}

export interface AvailableNetworks {
    [networkId: string]: {
        name: string;
        domain: string;
        bridgeUserId: string;
        isEnabled: boolean;
    };
}

export interface LinkedChannels {
    [networkId: string]: {
        channelName: string;
    }[];
}

export class IrcBridge {
    private static getNetworkId(network: CachedNetwork): string {
        return network.ircBridgeId + "-" + network.bridgeNetworkId;
    }

    public static parseNetworkId(networkId: string): { bridgeId: string, bridgeNetworkId: string } {
        const parts = networkId.split("-");
        const bridgeId = parts.splice(0, 1)[0];
        const bridgeNetworkId = parts.join("-");
        return {bridgeId, bridgeNetworkId};
    }

    constructor(private requestingUserId: string) {
    }

    public async isBridgingEnabled(): Promise<boolean> {
        const bridges = await IrcBridgeRecord.findAll({where: {isEnabled: true}});
        return !!bridges && bridges.length > 0;
    }

    public async hasNetworks(): Promise<boolean> {
        const allNetworks = (await this.getAllNetworks()).filter(n => n.isEnabled);
        return allNetworks.length > 0;
    }

    public async getNetworks(bridge?: IrcBridgeRecord, enabledOnly?: boolean): Promise<AvailableNetworks> {
        let networks = await this.getAllNetworks();
        if (bridge) networks = networks.filter(n => n.ircBridgeId === bridge.id);
        if (enabledOnly) networks = networks.filter(n => n.isEnabled);

        const available: AvailableNetworks = {};
        networks.forEach(n => available[IrcBridge.getNetworkId(n)] = {
            name: n.displayName,
            domain: n.domain,
            bridgeUserId: n.bridgeUserId,
            isEnabled: n.isEnabled,
        });
        return available;
    }

    public async getRoomConfiguration(inRoomId: string): Promise<IrcBridgeConfiguration> {
        const availableNetworks = await this.getNetworks(null, true);
        const bridges = await IrcBridgeRecord.findAll({where: {isEnabled: true}});
        const linkedChannels: LinkedChannels = {};

        for (const bridge of bridges) {
            const links = await this.fetchLinks(bridge, inRoomId);
            for (const key of Object.keys(links)) {
                linkedChannels[key] = links[key];
            }
        }

        return {availableNetworks: availableNetworks, links: linkedChannels};
    }

    public async getOperators(bridge: IrcBridgeRecord, networkId: string, channel: string): Promise<string[]> {
        const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId);
        if (!network) throw new Error("Network not found");

        const requestBody = {remote_room_server: network.domain, remote_room_channel: channel};

        let responses: ListOpsResponse[] = [];
        if (bridge.upstreamId) {
            const result = await this.doUpstreamRequest<ModularIrcResponse<ListOpsResponse>>(bridge, "POST", "/bridges/irc/_matrix/provision/querylink", null, requestBody);
            if (result && result.replies) responses = result.replies.map(r => r.response);
        } else {
            const result = await this.doProvisionRequest<ListOpsResponse>(bridge, "POST", "/_matrix/provision/querylink", null, requestBody);
            if (result) responses = [result];
        }

        const ops: string[] = [];
        for (const response of responses) {
            if (!response || !response.operators) continue;
            response.operators.forEach(i => ops.push(i));
        }

        return ops;
    }

    public async requestLink(bridge: IrcBridgeRecord, networkId: string, channel: string, op: string, inRoomId: string): Promise<any> {
        const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId);
        if (!network) throw new Error("Network not found");

        const requestBody = {
            remote_room_server: network.domain,
            remote_room_channel: channel,
            matrix_room_id: inRoomId,
            op_nick: op,
            user_id: this.requestingUserId,
        };

        if (bridge.upstreamId) {
            delete requestBody["user_id"];
            await this.doUpstreamRequest(bridge, "POST", "/bridges/irc/_matrix/provision/link", null, requestBody);
        } else {
            await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/link", null, requestBody);
        }
    }

    public async removeLink(bridge: IrcBridgeRecord, networkId: string, channel: string, inRoomId: string): Promise<any> {
        const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId);
        if (!network) throw new Error("Network not found");

        const requestBody = {
            remote_room_server: network.domain,
            remote_room_channel: channel,
            matrix_room_id: inRoomId,
            user_id: this.requestingUserId,
        };

        if (bridge.upstreamId) {
            delete requestBody["user_id"];
            await this.doUpstreamRequest(bridge, "POST", "/bridges/irc/_matrix/provision/unlink", null, requestBody);
        } else {
            await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/unlink", null, requestBody);
        }
    }

    private async getAllNetworks(): Promise<CachedNetwork[]> {
        const cached = Cache.for(CACHE_IRC_BRIDGE).get("networks");
        if (cached) return cached;

        const bridges = await IrcBridgeRecord.findAll();
        if (!bridges) return [];

        const networks: CachedNetwork[] = [];
        for (const bridge of bridges) {
            const bridgeNetworks = await this.fetchNetworks(bridge);
            bridgeNetworks.forEach(n => networks.push(n));
        }

        Cache.for(CACHE_IRC_BRIDGE).put("networks", networks, 60 * 60 * 1000); // 1 hour
        return networks;
    }

    private async fetchLinks(bridge: IrcBridgeRecord, inRoomId: string): Promise<LinkedChannels> {
        const availableNetworks = await this.getNetworks(bridge, true);
        const networksByDomain: { [domain: string]: { id: string, name: string, bridgeUserId: string } } = {};
        for (const key of Object.keys(availableNetworks)) {
            const network = availableNetworks[key];
            networksByDomain[network.domain] = {
                id: key,
                name: network.name,
                bridgeUserId: network.bridgeUserId,
            };
        }

        let responses: ListLinksResponseItem[] = [];
        if (bridge.upstreamId) {
            const result = await this.doUpstreamRequest<ModularIrcResponse<ListLinksResponseItem[]>>(bridge, "GET", "/bridges/irc/_matrix/provision/listlinks/" + inRoomId);
            if (result && result.replies) {
                const replies = result.replies.map(r => r.response);
                for (const reply of replies) reply.forEach(r => responses.push(r));
            }
        } else {
            const result = await this.doProvisionRequest<ListLinksResponseItem[]>(bridge, "GET", "/_matrix/provision/listlinks/" + inRoomId);
            if (result) responses = result;
        }

        const linked: LinkedChannels = {};
        for (const response of responses) {
            if (!response || !response.remote_room_server) continue;

            const network = networksByDomain[response.remote_room_server];
            if (!network) continue;

            if (!linked[network.id]) linked[network.id] = [];
            linked[network.id].push({
                channelName: response.remote_room_channel,
            });
        }

        return linked;
    }

    private async fetchNetworks(bridge: IrcBridgeRecord): Promise<CachedNetwork[]> {
        let responses: QueryNetworksResponse[] = [];
        if (bridge.upstreamId) {
            const result = await this.doUpstreamRequest<ModularIrcResponse<QueryNetworksResponse>>(bridge, "GET", "/bridges/irc/_matrix/provision/querynetworks");
            if (result && result.replies) responses = result.replies.map(r => r.response);
        } else {
            const result = await this.doProvisionRequest<QueryNetworksResponse>(bridge, "GET", "/_matrix/provision/querynetworks");
            if (result) responses = [result];
        }

        const networks: CachedNetwork[] = [];
        for (const response of responses) {
            if (!response || !response.servers) continue;

            for (const server of response.servers) {
                if (!server) continue;

                let existingNetwork = await IrcBridgeNetwork.findOne({
                    where: {
                        bridgeId: bridge.id,
                        bridgeNetworkId: server.network_id,
                    },
                });
                if (!existingNetwork) {
                    LogService.info("IrcBridge", "Discovered new network for bridge " + bridge.id + ": " + server.network_id);
                    existingNetwork = await IrcBridgeNetwork.create({
                        bridgeId: bridge.id,
                        isEnabled: false,
                        bridgeNetworkId: server.network_id,
                        bridgeUserId: server.bot_user_id,
                        displayName: server.desc,
                        domain: server.fields.domain,
                    });
                } else {
                    existingNetwork.displayName = server.desc;
                    existingNetwork.bridgeUserId = server.bot_user_id;
                    existingNetwork.domain = server.fields.domain;
                    await existingNetwork.save();
                }

                networks.push({
                    ircBridgeId: bridge.id,
                    bridgeNetworkId: existingNetwork.bridgeNetworkId,
                    bridgeUserId: existingNetwork.bridgeUserId,
                    displayName: existingNetwork.displayName,
                    domain: existingNetwork.domain,
                    isEnabled: existingNetwork.isEnabled,
                });
            }
        }

        return networks;
    }

    private async doUpstreamRequest<T>(bridge: IrcBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise<T> {
        const upstream = await Upstream.findByPk(bridge.upstreamId);
        const token = await UserScalarToken.findOne({
            where: {
                upstreamId: upstream.id,
                isDimensionToken: false,
                userId: this.requestingUserId,
            },
        });

        if (!qs) qs = {};
        qs["scalar_token"] = token.scalarToken;

        const apiUrl = upstream.apiUrl.endsWith("/") ? upstream.apiUrl.substring(0, upstream.apiUrl.length - 1) : upstream.apiUrl;
        const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint);
        LogService.info("IrcBridge", "Doing upstream IRC Bridge request: " + url);

        return new Promise<T>((resolve, reject) => {
            request({
                method: method,
                url: url,
                qs: qs,
                json: body,
            }, (err, res, _body) => {
                try {
                    if (err) {
                        LogService.error("IrcBridge", "Error calling " + url);
                        LogService.error("IrcBridge", err);
                        reject(err);
                    } else if (!res) {
                        LogService.error("IrcBridge", "There is no response for " + url);
                        reject(new Error("No response provided - is the service online?"));
                    } else if (res.statusCode !== 200) {
                        LogService.error("IrcBridge", "Got status code " + res.statusCode + " when calling " + url);
                        LogService.error("IrcBridge", res.body);
                        reject(new Error("Request failed"));
                    } else {
                        if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
                        resolve(res.body);
                    }
                } catch (e) {
                    LogService.error("IrcBridge", e);
                    reject(e);
                }
            });
        });
    }

    private async doProvisionRequest<T>(bridge: IrcBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise<T> {
        const provisionUrl = bridge.provisionUrl;
        const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl;
        const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint);
        LogService.info("IrcBridge", "Doing provision IRC Bridge request: " + url);

        return new Promise<T>((resolve, reject) => {
            request({
                method: method,
                url: url,
                qs: qs,
                json: body,
            }, (err, res, _body) => {
                try {
                    if (err) {
                        LogService.error("IrcBridge", "Error calling" + url);
                        LogService.error("IrcBridge", err);
                        reject(err);
                    } else if (!res) {
                        LogService.error("IrcBridge", "There is no response for " + url);
                        reject(new Error("No response provided - is the service online?"));
                    } else if (res.statusCode !== 200) {
                        LogService.error("IrcBridge", "Got status code " + res.statusCode + " when calling " + url);
                        LogService.error("IrcBridge", res.body);
                        reject(new Error("Request failed"));
                    } else {
                        if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
                        resolve(res.body);
                    }
                } catch (e) {
                    LogService.error("IrcBridge", e);
                    reject(e);
                }
            });
        });
    }
}
