import { OutPoint } from "@node-lightning/core";
import { IGossipEmitter, IWireMessage, MessageType } from "@node-lightning/wire";
import { ChannelAnnouncementMessage } from "@node-lightning/wire";
import { ChannelUpdateMessage } from "@node-lightning/wire";
import { NodeAnnouncementMessage } from "@node-lightning/wire";
import { EventEmitter } from "events";
import { Channel } from "./channel";
import { ChannelSettings } from "./channel-settings";
import { channelFromMessage } from "./deserialize/channel-from-message";
import { channelSettingsFromMessage } from "./deserialize/channel-settings-from-message";
import { Graph } from "./graph";
import { ChannelNotFoundError } from "./graph-error";
import { GraphError } from "./graph-error";
import { Node } from "./node";

// tslint:disable-next-line: interface-name
export declare interface GraphManager {
    on(event: "node", fn: (node: Node) => void): this;
    on(event: "channel", fn: (channel: Channel) => void): this;
    on(event: "channel_update", fn: (channel: Channel, settings: ChannelSettings) => void): this;
    on(event: "error", fn: (err: GraphError) => void): this;
}

/**
 * GraphManager is a facade around a Graph object. It converts in-bound
 * gossip messages from the wire into a graph representation. Channels
 * can also be removed by monitoring the block chain via a chainmon object.
 */
export class GraphManager extends EventEmitter {
    public graph: Graph;
    public gossipEmitter: IGossipEmitter;

    constructor(gossipManager: IGossipEmitter, graph = new Graph()) {
        super();
        this.graph = graph;
        this.gossipEmitter = gossipManager;
        this.gossipEmitter.on("message", this._onMessage.bind(this));
    }

    /**
     * Closes channel via the outpoint
     * @param outpoint
     */
    public removeChannel(outpoint: OutPoint) {
        const outpointStr = outpoint.toString();
        for (const channel of this.graph.channels.values()) {
            if (outpointStr === channel?.channelPoint?.toString()) {
                this.graph.removeChannel(channel);
                this.emit("channel_closed", channel);
                return;
            }
        }
    }

    private _onMessage(msg: IWireMessage) {
        // channel_announcement messages are processed by:
        // First ensuring that we don't already have a duplicate channel.
        // We then check to see if we need to insert node
        // references. Inserting temporary node's is required because we
        // may receieve a channel_announcement without ever receiving
        // node_announcement messages.

        if (isChannelAnnouncment(msg)) {
            const channel = channelFromMessage(msg);

            // abort processing if the channel already exists
            if (this.graph.getChannel(msg.shortChannelId)) {
                return;
            }

            // construct node1 if required
            if (!this.graph.getNode(msg.nodeId1)) {
                const node1 = new Node();
                node1.nodeId = msg.nodeId1;
                this.graph.addNode(node1);
                this.emit("node", node1);
            }

            // construct node2 if required
            if (!this.graph.getNode(msg.nodeId2)) {
                const node2 = new Node();
                node2.nodeId = msg.nodeId2;
                this.graph.addNode(node2);
                this.emit("node", node2);
            }

            // finally attach the channel
            this.graph.addChannel(channel);
            this.emit("channel", channel);
            return;
        }

        // channel_update messages are processed by:
        // * looking for the existing channel, if it doesn't then an error is thrown.
        // * updating the existing channel
        // The GossipFilter in Wire should ensure that channel_announcement messages
        // are always transmitted prior to channel_update messages being announced.
        if (isChannelUpdate(msg)) {
            // first validate we have a channel
            const channel = this.graph.getChannel(msg.shortChannelId);
            if (!channel) {
                this.emit("error", new ChannelNotFoundError(msg.shortChannelId));
                return;
            }

            // construct the settings and update the channel
            const settings = channelSettingsFromMessage(msg);
            channel.updateSettings(settings);
            this.emit("channel_update", channel, settings);
            return;
        }

        // node_announcement messages are processed by:
        // * finding or creating the node (if it doesn't exist)
        // * updating the node with values from the announcement
        if (isNodeAnnouncement(msg)) {
            let node = this.graph.getNode(msg.nodeId);
            if (!node) {
                node = new Node();
                node.nodeId = msg.nodeId;
                this.graph.addNode(node);
            }
            node.features = msg.features;
            node.lastUpdate = msg.timestamp;
            node.alias = msg.alias;
            node.rgbColor = msg.rgbColor;
            node.addresses = msg.addresses;
            this.emit("node", node);
        }
    }
}

function isChannelAnnouncment(msg: IWireMessage): msg is ChannelAnnouncementMessage {
    return msg.type === MessageType.ChannelAnnouncement;
}

function isChannelUpdate(msg: IWireMessage): msg is ChannelUpdateMessage {
    return msg.type === MessageType.ChannelUpdate;
}

function isNodeAnnouncement(msg: IWireMessage): msg is NodeAnnouncementMessage {
    return msg.type === MessageType.NodeAnnouncement;
}
