import { EventEmitter } from 'events'
import { Logger, LoggerOptions } from 'node-log-it'
import { merge, filter, remove, meanBy, round } from 'lodash'
import { RpcDelegate } from '../delegates/rpc-delegate'
import C from '../common/constants'
import { NeoValidator } from '../validators/neo-validator'
import { AxiosRequestConfig } from 'axios'

const MODULE_NAME = 'Node'
const DEFAULT_ID = 0
const DEFAULT_OPTIONS: NodeOptions = {
  toLogReliability: false,
  truncateRequestLogIntervalMs: 30 * 1000,
  requestLogTtl: 5 * 60 * 1000, // In milliseconds
  timeout: 30000,
  loggerOptions: {},
}

export interface NodeMeta {
  isActive: boolean | undefined
  pendingRequests: number | undefined
  latency: number | undefined
  blockHeight: number | undefined
  lastSeenTimestamp: number | undefined
  userAgent: string | undefined
  endpoint: string
}

export interface NodeOptions {
  toLogReliability?: boolean
  truncateRequestLogIntervalMs?: number
  requestLogTtl?: number
  timeout?: number
  loggerOptions?: LoggerOptions
}

export class Node extends EventEmitter {
  isActive: boolean | undefined
  pendingRequests: number | undefined
  latency: number | undefined // In milliseconds
  blockHeight: number | undefined
  lastPingTimestamp: number | undefined // Latest timestamp that node perform benchmark on
  lastSeenTimestamp: number | undefined // Latest timestamp that node detected to be activated via benchmark
  userAgent: string | undefined
  endpoint: string
  isBenchmarking = false

  private options: NodeOptions
  private logger: Logger
  private requestLogs: object[] = []
  private truncateRequestLogIntervalId?: NodeJS.Timer

  constructor(endpoint: string, options: NodeOptions = {}) {
    super()

    // Associate required properties
    this.endpoint = endpoint

    // Associate optional properties
    this.options = merge({}, DEFAULT_OPTIONS, options)
    this.validateOptionalParameters()

    // Bootstrapping
    this.logger = new Logger(MODULE_NAME, this.options.loggerOptions)
    if (this.options.toLogReliability) {
      this.truncateRequestLogIntervalId = setInterval(() => this.truncateRequestLog(), this.options.truncateRequestLogIntervalMs!)
    }

    // Event handlers
    this.on('query:init', this.queryInitHandler.bind(this))
    this.on('query:complete', this.queryCompleteHandler.bind(this))
    this.logger.debug('constructor completes.')
  }

  async getBlock(height: number, isVerbose: boolean = true): Promise<object> {
    this.logger.debug('getBlock triggered.')

    NeoValidator.validateHeight(height)

    const verboseKey: number = isVerbose ? 1 : 0
    return await this.query(C.rpc.getblock, [height, verboseKey])
  }

  async getBlockCount(): Promise<object> {
    this.logger.debug('getBlockCount triggered.')
    return await this.query(C.rpc.getblockcount)
  }

  async getVersion(): Promise<object> {
    this.logger.debug('getVersion triggered.')
    return await this.query(C.rpc.getversion)
  }

  async getTransaction(transactionId: string, isVerbose: boolean = true): Promise<object> {
    this.logger.debug('transactionId triggered.')
    const verboseKey: number = isVerbose ? 1 : 0
    return await this.query(C.rpc.getrawtransaction, [transactionId, verboseKey])
  }

  getNodeMeta(): NodeMeta {
    return {
      isActive: this.isActive,
      pendingRequests: this.pendingRequests,
      latency: this.latency,
      blockHeight: this.blockHeight,
      lastSeenTimestamp: this.lastSeenTimestamp,
      userAgent: this.userAgent,
      endpoint: this.endpoint,
    }
  }

  /**
   * A float number between 0 an 1
   */
  getNodeReliability(): number | undefined {
    const requestCount = this.requestLogs.length
    if (requestCount === 0) {
      return undefined
    }

    const successCount = filter(this.requestLogs, (logObj: any) => logObj.isSuccess === true).length
    return successCount / requestCount
  }

  getShapedLatency(): number | undefined {
    this.logger.debug('getShapedLatency triggered.')
    if (this.requestLogs.length === 0) {
      return undefined
    }

    const logPool = filter(this.requestLogs, (logObj: any) => logObj.isSuccess === true && logObj.latency !== undefined)
    if (logPool.length === 0) {
      return undefined
    }

    const averageLatency = round(meanBy(logPool, (logObj: any) => logObj.latency), 0)
    return averageLatency
  }

  close() {
    this.logger.debug('close triggered.')
    if (this.truncateRequestLogIntervalId) {
      clearInterval(this.truncateRequestLogIntervalId)
    }
  }

  private queryInitHandler(payload: object) {
    this.logger.debug('queryInitHandler triggered.')
    this.startBenchmark(payload)
  }

  private queryCompleteHandler(payload: any) {
    this.logger.debug('queryCompleteHandler triggered.')
    this.stopBenchmark(payload)
  }

  private validateOptionalParameters() {
    // TODO
  }

  private startBenchmark(payload: any) {
    this.logger.debug('startBenchmark triggered.')
    this.increasePendingRequest()

    // Perform latency benchmark when it's a getBlockCount() request
    if (payload.method === C.rpc.getblockcount) {
      if (this.isBenchmarking) {
        this.logger.debug('An benchmarking schedule is already in place. Skipping... endpoint:', this.endpoint)
      } else {
        this.isBenchmarking = true
      }
    }
  }

  private stopBenchmark(payload: any) {
    this.logger.debug('stopBenchmark triggered.')
    this.decreasePendingRequest()
    this.lastPingTimestamp = Date.now()

    // Store latest active state base on existence of error
    if (!payload.isSuccess) {
      this.isActive = false
    } else {
      this.isActive = true
      this.lastSeenTimestamp = Date.now()
    }

    // Store block height value if provided
    if (payload.blockHeight) {
      this.blockHeight = payload.blockHeight
    }

    // Store user agent value if provided
    if (payload.userAgent) {
      this.userAgent = payload.userAgent
    }

    // Perform benchmark when it's a getBlockCount() request
    if (payload.method === C.rpc.getblockcount) {
      if (!this.isBenchmarking) {
        this.logger.debug('There are no running benchmarking schedule in place. Skipping... endpoint:', this.endpoint)
      } else {
        this.isBenchmarking = false

        // Store latency value if provided
        if (payload.latency) {
          this.latency = payload.latency
        }

        // Reliability logging
        if (this.options.toLogReliability) {
          if (!payload.isSuccess) {
            this.requestLogs.push({
              timestamp: Date.now(),
              isSuccess: payload.isSuccess,
            })
          } else {
            this.requestLogs.push({
              timestamp: Date.now(),
              isSuccess: payload.isSuccess,
              latency: this.latency,
            })
          }
        }
      }
    }
  }

  private truncateRequestLog() {
    this.logger.debug('truncateRequestLog triggered.')
    const cutOffTimestamp = Date.now() - this.options.requestLogTtl!
    this.requestLogs = remove(this.requestLogs, (logObj: any) => logObj.timestamp > cutOffTimestamp)
  }

  private async query(method: string, params: any[] = [], id: number = DEFAULT_ID): Promise<object> {
    this.logger.debug('query triggered. method:', method)
    this.emit('query:init', { method, params, id })
    const requestConfig = this.getRequestConfig()
    const t0 = Date.now()
    try {
      const res: any = await RpcDelegate.query(this.endpoint, method, params, id, requestConfig)
      const latency = Date.now() - t0
      const result = res.result
      const blockHeight = method === C.rpc.getblockcount ? result : undefined
      const userAgent = method === C.rpc.getversion ? result.useragent : undefined
      this.emit('query:complete', { isSuccess: true, method, latency, blockHeight, userAgent })
      return result
    } catch (err) {
      this.emit('query:complete', { isSuccess: false, method, error: err })
      throw err
    }
  }

  private increasePendingRequest() {
    this.logger.debug('increasePendingRequest triggered.')
    if (this.pendingRequests) {
      this.pendingRequests += 1
    } else {
      this.pendingRequests = 1
    }
  }

  private decreasePendingRequest() {
    this.logger.debug('decreasePendingRequest triggered.')
    if (this.pendingRequests) {
      this.pendingRequests -= 1
    } else {
      this.pendingRequests = 0
    }
  }

  private getRequestConfig(): AxiosRequestConfig {
    const config: AxiosRequestConfig = {}
    if (this.options.timeout) {
      config.timeout = this.options.timeout
    }
    return config
  }
}
