import { HashValue, OutPoint } from '@node-dlc/bitcoin';

import { ChannelAnnouncementMessage } from '../messages/ChannelAnnouncementMessage';
import { ChannelUpdateMessage } from '../messages/ChannelUpdateMessage';
import { ExtendedChannelAnnouncementMessage } from '../messages/ExtendedChannelAnnouncementMessage';
import { IWireMessage } from '../messages/IWireMessage';
import { NodeAnnouncementMessage } from '../messages/NodeAnnouncementMessage';
import { MessageType } from '../MessageType';
import { fundingScript } from '../ScriptUtils';
import { WireError, WireErrorCode } from '../WireError';
import { IGossipStore } from './GossipStore';
import { IGossipFilterChainClient } from './IGossipFilterChainClient';

export class Result<V, E> {
  public static err<V, E>(error: E): Result<V, E> {
    return new Result<V, E>(undefined, error);
  }

  public static ok<V, E>(value: V): Result<V, E> {
    return new Result<V, E>(value);
  }

  constructor(readonly value?: V, readonly error?: E) {}

  public get isOk(): boolean {
    return this.value !== undefined;
  }

  public get isErr(): boolean {
    return this.error !== undefined;
  }
}

export type GossipFilterResult = Result<IWireMessage[], WireError>;

/**
 * GossipFilter recieves messages from peers and performs validation
 * on the messages to ensure that they are valid messages. These validation
 * follow messaging rules defined in Bolt #7 and include things like
 * signature checks, on-chain validation, and message sequencing requirements.
 * Successful message validation results in messages being written to an
 * instance of IGossipStore and returned as results
 *
 * The GossipFilter will also store pending messages, such as channel_update
 * message arriving before the channel_announcement.
 */
export class GossipFilter {
  constructor(
    readonly gossipStore: IGossipStore,
    readonly pendingStore: IGossipStore,
    readonly chainClient?: IGossipFilterChainClient,
  ) {}

  /**
   * Validates a message and writes it to the appropriate store.
   * A fully processed messages (or releated messages) will be
   * returned when a message is successfully processed.
   *
   */
  public async validateMessage(msg: IWireMessage): Promise<GossipFilterResult> {
    switch (msg.type) {
      case MessageType.ChannelAnnouncement:
        return await this._validateChannelAnnouncement(
          msg as ChannelAnnouncementMessage,
        );
      case MessageType.NodeAnnouncement:
        return await this._validateNodeAnnouncement(
          msg as NodeAnnouncementMessage,
        );
      case MessageType.ChannelUpdate:
        return await this._validateChannelUpdate(msg as ChannelUpdateMessage);
    }
    return Result.ok([] as IWireMessage[]);
  }

  /**
   * Validate a node announcement message by checking to see if the
   * message is newer than the prior timestamp and if the message
   * has a valid signature from the corresponding node
   */
  private async _validateNodeAnnouncement(
    msg: NodeAnnouncementMessage,
  ): Promise<GossipFilterResult> {
    // get or construct a node
    const existing = await this.gossipStore.findNodeAnnouncement(msg.nodeId);

    // check if the message is newer than the last update
    if (existing && existing.timestamp >= msg.timestamp)
      return Result.ok([] as IWireMessage[]);

    // queue node if we don't have any channels
    const scids = await this.gossipStore.findChannelsForNode(msg.nodeId);
    if (!scids.length) {
      await this.pendingStore.saveNodeAnnouncement(msg);
      return Result.ok([] as IWireMessage[]);
    }

    // validate message signature
    if (!NodeAnnouncementMessage.verifySignatures(msg)) {
      return Result.err(new WireError(WireErrorCode.nodeAnnSigFailed, [msg]));
    }

    // save the announcement
    await this.gossipStore.saveNodeAnnouncement(msg);

    // broadcast valid message
    return Result.ok([msg]);
  }

  /**
   * Validates a ChannelAnnouncementMessage by verifying the signatures
   * and validating the transaction on chain work. This message will
   */
  private async _validateChannelAnnouncement(
    msg: ChannelAnnouncementMessage,
  ): Promise<GossipFilterResult> {
    // attempt to find the existing chan_ann message
    const existing = await this.gossipStore.findChannelAnnouncement(
      msg.shortChannelId,
    );

    // If the message is an extended message and it has populated the outpoint and capacity we
    // can skip message processing becuase there is nothing left to do.
    if (existing && existing instanceof ExtendedChannelAnnouncementMessage) {
      return Result.ok([] as IWireMessage[]);
    }

    // If there is an existing message and we don't have a chain_client then we want
    // to abort processing to prevent from populating the gossip store with chan_ann
    // messages that do not have a extended information. Alterntaively, if we DO have an
    // an existing message that is just a chan_ann and not ext_chan_ann and there is a
    // chain_client, we want to update the chan_ann with onchain information
    if (existing && !this.chainClient) {
      return Result.ok([] as IWireMessage[]);
    }

    // validate signatures for message
    if (!ChannelAnnouncementMessage.verifySignatures(msg)) {
      return Result.err(new WireError(WireErrorCode.chanAnnSigFailed, [msg]));
    }

    if (this.chainClient) {
      // load the block hash for the block height
      const blockHash = await this.chainClient.getBlockHash(
        msg.shortChannelId.block,
      );
      if (!blockHash) {
        return Result.err(new WireError(WireErrorCode.chanBadBlockHash, [msg]));
      }

      // load the block details so we can find the tx
      const block = await this.chainClient.getBlock(blockHash);
      if (!block) {
        return Result.err(
          new WireError(WireErrorCode.chanBadBlock, [msg, blockHash]),
        );
      }

      // load the txid from the block details
      const txId = block.tx[msg.shortChannelId.txIdx];
      if (!txId) {
        return Result.err(new WireError(WireErrorCode.chanAnnBadTx, [msg]));
      }

      // obtain a UTXO to verify the tx hasn't been spent yet
      const utxo = await this.chainClient.getUtxo(
        txId,
        msg.shortChannelId.voutIdx,
      );
      if (!utxo) {
        return Result.err(new WireError(WireErrorCode.chanUtxoSpent, [msg]));
      }

      // verify the tx script is a p2ms
      const expectedScript = fundingScript([msg.bitcoinKey1, msg.bitcoinKey2]);
      const actualScript = Buffer.from(utxo.scriptPubKey.hex, 'hex');
      if (!expectedScript.equals(actualScript)) {
        return Result.err(
          new WireError(WireErrorCode.chanBadScript, [
            msg,
            expectedScript,
            actualScript,
          ]),
        );
      }

      // construct outpoint
      const outpoint = new OutPoint(
        HashValue.fromRpc(txId),
        msg.shortChannelId.voutIdx,
      );

      // calculate capacity in satoshi
      // Not sure about this code. MAX_SAFE_INTEGER is still safe
      // for Bitcoin satoshi.
      const capacity = BigInt(Math.round(utxo.value * 10 ** 8));

      // overright msg with extended channel_announcement
      const extended = ExtendedChannelAnnouncementMessage.fromMessage(msg);
      extended.outpoint = outpoint;
      extended.capacity = capacity;
      msg = extended;
    }

    // save channel_ann
    await this.gossipStore.saveChannelAnnouncement(msg);

    // broadcast valid message
    const results: IWireMessage[] = [msg];

    // process outstanding node messages
    const pendingUpdates = [
      await this.pendingStore.findChannelUpdate(msg.shortChannelId, 0),
      await this.pendingStore.findChannelUpdate(msg.shortChannelId, 1),
    ];
    for (const pendingUpdate of pendingUpdates) {
      if (pendingUpdate) {
        await this.pendingStore.deleteChannelUpdate(
          pendingUpdate.shortChannelId,
          pendingUpdate.direction,
        );
        const result = await this._validateChannelUpdate(pendingUpdate);
        if (result.isErr) return result;
        else results.push(...result.value);
      }
    }

    // process outstanding node messages
    const pendingNodeAnns = [
      await this.pendingStore.findNodeAnnouncement(msg.nodeId1),
      await this.pendingStore.findNodeAnnouncement(msg.nodeId2),
    ];
    for (const pendingNodeAnn of pendingNodeAnns) {
      if (pendingNodeAnn) {
        await this.pendingStore.deleteNodeAnnouncement(pendingNodeAnn.nodeId);
        const result = await this._validateNodeAnnouncement(pendingNodeAnn);
        if (result.isErr) return result;
        else results.push(...result.value);
      }
    }

    return Result.ok(results);
  }

  /**
   * Validates a channel_update message by ensuring that:
   * - the channel_announcement has already been recieved
   * - the channel_update is not old
   * - the channel_update is correctly signed by the node
   */
  private async _validateChannelUpdate(
    msg: ChannelUpdateMessage,
  ): Promise<GossipFilterResult> {
    // Ensure a channel announcement exists. If it does not,
    // we need to queue the update message until the channel announcement
    // can be adequately processed. Technically according to the specification in
    // Bolt 07, we should ignore channel_update message if we not processed
    // a valid channel_announcement. In reality, we may end up in a situation
    // where a channel_update makes it to our peer prior to the channel_announcement
    // being received.
    const channelMessage = await this.gossipStore.findChannelAnnouncement(
      msg.shortChannelId,
    );
    if (!channelMessage) {
      await this.pendingStore.saveChannelUpdate(msg);
      return Result.ok([] as IWireMessage[]);
    }

    // ignore out of date message
    const existingUpdate = await this.gossipStore.findChannelUpdate(
      msg.shortChannelId,
      msg.direction,
    );
    if (existingUpdate && existingUpdate.timestamp >= msg.timestamp) {
      return Result.ok([] as IWireMessage[]);
    }

    // validate message signature for the node in
    const nodeId =
      msg.direction === 0 ? channelMessage.nodeId1 : channelMessage.nodeId2;
    if (!ChannelUpdateMessage.validateSignature(msg, nodeId)) {
      return Result.err(
        new WireError(WireErrorCode.chanUpdSigFailed, [msg, nodeId]),
      );
    }

    // save the message
    await this.gossipStore.saveChannelUpdate(msg);

    // broadcast valid message
    return Result.ok([msg]);
  }
}
