import {LogCategory, logDebug, logError, logWarn} from "../log";
import {BasicIPCHandler, ChannelMessage, IPCChannel} from "../ipc/BrowserIPC";
import {guid} from "../crypto/uid";
import {tr} from "tc-shared/i18n/localize";

export type ConnectRequestData = {
    address: string;

    profile?: string;
    username?: string;

    password?: {
        value: string;
        hashed: boolean;
    };
}

export interface ConnectOffer {
    request_id: string;
    data: ConnectRequestData;
}

export interface ConnectOfferAnswer {
    request_id: string;
    accepted: boolean;
}

export interface ConnectExecute {
    request_id: string;
}

export interface ConnectExecuted {
    request_id: string;
    succeeded: boolean;
    message?: string;
}

/* The connect process:
 *  1. Broadcast an offer
 *  2. Wait 50ms for all offer responses or until the first one respond with "ok"
 *  3. Select (if possible) on accepted offer and execute the connect
 */
export class ConnectHandler {
    private static readonly CHANNEL_NAME = "connect";

    readonly ipc_handler: BasicIPCHandler;
    private ipc_channel: IPCChannel;

    public callback_available: (data: ConnectRequestData) => boolean = () => false;
    public callback_execute: (data: ConnectRequestData) => boolean | string = () => false;


    private _pending_connect_offers: {
        id: string;
        data: ConnectRequestData;
        timeout: number;

        remote_handler: string;
    }[] = [];

    private _pending_connects_requests: {
        id: string;

        data: ConnectRequestData;
        timeout: number;

        callback_success: () => any;
        callback_failed: (message: string) => any;
        callback_avail: () => Promise<boolean>;

        remote_handler?: string;
    }[] = [];

    constructor(ipc_handler: BasicIPCHandler) {
        this.ipc_handler = ipc_handler;
    }

    public setup() {
        this.ipc_channel = this.ipc_handler.createChannel(ConnectHandler.CHANNEL_NAME);
        this.ipc_channel.messageHandler = this.onMessage.bind(this);
    }

    private onMessage(sender: string, broadcast: boolean, message: ChannelMessage) {
        if(broadcast) {
            if(message.type == "offer") {
                const data = message.data as ConnectOffer;

                const response = {
                    accepted: this.callback_available(data.data),
                    request_id: data.request_id
                } as ConnectOfferAnswer;

                if(response.accepted) {
                    logDebug(LogCategory.IPC, tr("Received new connect offer from %s: %s"), sender, data.request_id);

                    const ld = {
                        remote_handler: sender,
                        data: data.data,
                        id: data.request_id,
                        timeout: 0
                    };
                    this._pending_connect_offers.push(ld);
                    ld.timeout = setTimeout(() => {
                        logDebug(LogCategory.IPC, tr("Dropping connect request %s, because we never received an execute."), ld.id);
                        this._pending_connect_offers.remove(ld);
                    }, 120 * 1000) as any;
                }
                this.ipc_channel.sendMessage("offer-answer", response, sender);
            }
        } else {
            if(message.type == "offer-answer") {
                const data = message.data as ConnectOfferAnswer;
                const request = this._pending_connects_requests.find(e => e.id === data.request_id);
                if(!request) {
                    logWarn(LogCategory.IPC, tr("Received connect offer answer with unknown request id (%s)."), data.request_id);
                    return;
                }
                if(!data.accepted) {
                    logDebug(LogCategory.IPC, tr("Client %s rejected the connect offer (%s)."), sender, request.id);
                    return;
                }
                if(request.remote_handler) {
                    logDebug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s), but offer has already been accepted."), sender, request.id);
                    return;
                }

                logDebug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s). Request local acceptance."), sender, request.id);
                request.remote_handler = sender;
                clearTimeout(request.timeout);

                request.callback_avail().then(flag => {
                    if(!flag) {
                        request.callback_failed("local avail rejected");
                        return;
                    }

                    logDebug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler);
                    this.ipc_channel.sendMessage("execute", {
                        request_id: request.id
                    } as ConnectExecute, request.remote_handler);
                    request.timeout = setTimeout(() => {
                        request.callback_failed("connect execute timeout");
                    }, 1000) as any;
                }).catch(error => {
                    logError(LogCategory.IPC, tr("Local avail callback caused an error: %o"), error);
                    request.callback_failed(tr("local avail callback caused an error"));
                });

            }
            else if(message.type == "executed") {
                const data = message.data as ConnectExecuted;
                const request = this._pending_connects_requests.find(e => e.id === data.request_id);
                if(!request) {
                    logWarn(LogCategory.IPC, tr("Received connect executed with unknown request id (%s)."), data.request_id);
                    return;
                }

                if(request.remote_handler != sender) {
                    logWarn(LogCategory.IPC, tr("Received connect executed for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler);
                    return;
                }

                logDebug(LogCategory.IPC, tr("Received connect executed response from client %s for request %s. Succeeded: %o (%s)"), sender, data.request_id, data.succeeded, data.message);
                clearTimeout(request.timeout);
                if(data.succeeded)
                    request.callback_success();
                else
                    request.callback_failed(data.message);
            }
            else if(message.type == "execute") {
                const data = message.data as ConnectExecute;
                const request = this._pending_connect_offers.find(e => e.id === data.request_id);
                if(!request) {
                    logWarn(LogCategory.IPC, tr("Received connect execute with unknown request id (%s)."), data.request_id);
                    return;
                }

                if(request.remote_handler != sender) {
                    logWarn(LogCategory.IPC, tr("Received connect execute for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler);
                    return;
                }
                clearTimeout(request.timeout);
                this._pending_connect_offers.remove(request);

                logDebug(LogCategory.IPC, tr("Executing connect for %s"), data.request_id);
                const cr = this.callback_execute(request.data);

                const response = {
                    request_id: data.request_id,

                    succeeded: typeof(cr) !== "string" && cr,
                    message: typeof(cr) === "string" ? cr : "",
                } as ConnectExecuted;
                this.ipc_channel.sendMessage("executed", response, request.remote_handler);
            }
        }
    }

    post_connect_request(data: ConnectRequestData, callback_avail: () => Promise<boolean>) : Promise<void> {
        return new Promise<void>((resolve, reject) => {
            const pd = {
                data: data,
                id: guid(),
                timeout: 0,

                callback_success: () => {
                    this._pending_connects_requests.remove(pd);
                    clearTimeout(pd.timeout);
                    resolve();
                },

                callback_failed: error => {
                    this._pending_connects_requests.remove(pd);
                    clearTimeout(pd.timeout);
                    reject(error);
                },

                callback_avail: callback_avail,
            };
            this._pending_connects_requests.push(pd);

            this.ipc_channel.sendMessage("offer", {
                request_id: pd.id,
                data: pd.data
            } as ConnectOffer);
            pd.timeout = setTimeout(() => {
                pd.callback_failed("received no response to offer");
            }, 50) as any;
        })
    }
}