import { ILogger } from '@node-dlc/logger';
import { Readable } from 'stream';

import { InitFeatureFlags } from '../flags/InitFeatureFlags';
import { IWireMessage } from '../messages/IWireMessage';
import { MessageType } from '../MessageType';
import { IPeer, Peer } from '../Peer';
import { PeerState } from '../PeerState';
import { GossipError, GossipErrorCode } from './GossipError';
import { GossipFilter } from './GossipFilter';
import { GossipQueriesReceiver } from './GossipQueriesReceiver';
import { GossipQueriesSync } from './GossipQueriesSync';

export enum ReadState {
  Ready,
  Reading,
  Blocked,
}

/**
 * Implements a decorator for the standard Peer adding functionality for Gossip
 * related activities. Messages from the GossipPeer occur after being pushed
 * through a GossipFilter to validate they are aokay.
 */
export class GossipPeer extends Readable implements IPeer {
  public readonly logger: ILogger;

  private _readState: ReadState = ReadState.Ready;
  private _receiver: GossipQueriesReceiver;
  private _syncRangeTask: GossipQueriesSync;

  /**
   * This class expects to be instantiated by a peer that is read and will
   * throw if the peer is not yet in the ready state. This ensures we can
   * construct the GossipPeer correctly using the negotiated features
   * @param peer
   * @param filter
   * @param logger
   */
  constructor(
    readonly peer: Peer,
    readonly filter: GossipFilter,
    logger: ILogger,
  ) {
    super({ objectMode: true, highWaterMark: peer.highWaterMark });

    // Enforce that the peer is ready to rock
    if (peer.state !== PeerState.Ready) {
      throw new GossipError(GossipErrorCode.PeerNotReady);
    }

    // Construct a sub-logger for use by the Gossiping systems
    this.logger = logger.sub('gspeer', peer.id);

    // Attach the appropriate events, many of them will simply be forwarded
    // but we will intercept messages and funnel appropriate messages
    // through the filter.
    this.peer.on('readable', this._onPeerReadable.bind(this));
    this.peer.on('close', () => this.emit('close'));
    if (this.gossipQueries) {
      this._receiver = new GossipQueriesReceiver(
        this.peer.localFeatures[0],
        this.peer,
        this.logger,
      );
    }
  }

  /**
   * Returns true if the gossip_queries feature has been negotiated with the
   * remote peer.
   */
  public get gossipQueries(): boolean {
    return (
      this.peer.remoteFeatures.isSet(InitFeatureFlags.gossipQueriesOptional) ||
      this.peer.remoteFeatures.isSet(InitFeatureFlags.gossipQueriesRequired)
    );
  }

  /**
   * Performs Gossip synchronization using the negotiated strategy. Currently
   * only support gossip_queries
   * @param firstBlock
   * @param numBlocks
   */
  public async syncRange(
    firstBlock?: number,
    numBlocks?: number,
  ): Promise<boolean> {
    if (this.gossipQueries) {
      const chainHash = this.peer.localChains[0];
      this._syncRangeTask = new GossipQueriesSync(chainHash, this, this.logger);
      await this._syncRangeTask.queryRange(firstBlock, numBlocks);
      this._syncRangeTask = undefined;
      return true;
    }
    return false;
  }

  /**
   * Enables the receipt of rumor mongered messages.
   */
  public enableGossip(): void {
    if (this.gossipQueries) {
      this._receiver.activate();
    }
  }

  /**
   * Disables the receipt of rumor mongered messages.
   */
  public disableGossip(): void {
    if (this.gossipQueries) {
      this._receiver.deactivate();
    }
  }

  /**
   * Sends the serialized data to the peer.
   * @param buf
   */
  public send(buf: Buffer): void {
    this.peer.send(buf);
  }

  /**
   * Sends a message to the peer using the default serialization.
   * @param msg
   */
  public sendMessage(msg: IWireMessage): void {
    this.peer.sendMessage(msg);
  }

  /**
   * Disconnects the peer
   */
  public disconnect(): void {
    this.peer.disconnect();
  }

  /**
   * Fires when a peer triggers the readable event. This method locks
   * to ensure only a single read event occurs at a time.
   * @returns
   */
  protected async _onPeerReadable(): Promise<void> {
    while (this._readState === ReadState.Ready) {
      this._readState = ReadState.Reading;
      const msg = this.peer.read() as IWireMessage;
      if (!msg) {
        this._readState = ReadState.Ready;
        return;
      }

      const filteredMsgs = await this._onPeerMessage(msg);
      this._readState = ReadState.Ready;

      for (const filteredMsg of filteredMsgs) {
        if (!this.push(filteredMsg)) {
          this._readState = ReadState.Blocked;
        }
      }
    }
  }

  /**
   * Internally process messages. If the message is a routing related message
   * it will pass through the GossipFilter, otherwise it will be immediately
   * broadcast.
   * @param msg
   */
  protected async _onPeerMessage(msg: IWireMessage): Promise<IWireMessage[]> {
    // HACK: we're adding the sync task message tracking here
    if (this._syncRangeTask) {
      this._syncRangeTask.handleWireMessage(msg);
    }

    // Run gossip message through the filter
    if (
      msg.type === MessageType.ChannelAnnouncement ||
      msg.type === MessageType.ChannelUpdate ||
      msg.type === MessageType.NodeAnnouncement
    ) {
      try {
        const result = await this.filter.validateMessage(msg);

        if (result.isOk) {
          return result.value;
        } else {
          // Handled error should be emitted to the caller
          // but we prevent the transform stream from
          // stopping by calling the callback without an
          // error.
          this.emit('gossip_error', result.error);
        }

        // Unhandled error is something unexpected and our peer
        // is now in a broken state and we need to disconnect.
      } catch (err) {
        this.disconnect();
        this.emit('error', err);
        return [];
      }
    }
    return [msg];
  }

  public _read(): void {
    // This method will be triggered when .read() is called by the
    // consumer. Therefore we can consider the read unblocked.
    if (this._readState === ReadState.Blocked) {
      this._readState = ReadState.Ready;
    }
    // Trigger a read but wait until the end of the event loop.
    // This is necessary when reading in paused mode where
    // _read was triggered by stream.read() originating inside
    // a "readable" event handler. Attempting to push more data
    // synchronously will not trigger another "readable" event.
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
    setImmediate(() => this._onPeerReadable());
  }
}
