import { MsgView } from '../../buffer'
import { MsgTag, MsgType, SessionRejectReason } from '../../types'
import { IJsFixConfig } from '../../config'
import { FixSession } from '../session/fix-session'
import { FixMsgAsciiStoreResend, FixMsgMemoryStore, IFixMsgStore, IFixMsgStoreRecord } from '../../store'
import { SessionState } from '../tcp'
import { TickAction } from '../tick-action'
import { IMsgApplication } from '../msg-application'
import { SegmentType } from '../../buffer/segment/segment-type'

export abstract class AsciiSession extends FixSession {
  public heartbeat: boolean = true
  protected store: IFixMsgStore | null = null
  protected resender: FixMsgAsciiStoreResend

  protected constructor (public readonly config: IJsFixConfig) {
    super(config)
    this.requestLogoutType = this.respondLogoutType = MsgType.Logout
    this.requestLogonType = MsgType.Logon
    this.store = new FixMsgMemoryStore(this.config.description.SenderCompId, this.config)
    this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
  }

  private checkSeqNo (msgType: string, view: MsgView): boolean {
    switch (msgType) {
      case MsgType.SequenceReset: {
        return true
      }

      default: {
        const state = this.sessionState
        const lastSeq: number = state.lastPeerMsgSeqNum
        const seqNo: number = view.getTyped(MsgTag.MsgSeqNum) as number
        let ret: boolean = false
        const seqDelta: number = seqNo - lastSeq
        if (seqDelta <= 0) {
          // serious problem ... drop immediately
          this.sessionLogger.warning(`terminate as seqDelta (${seqDelta}) < 0 lastSeq = ${lastSeq} seqNo = ${seqNo}`)
          this.stop()
        } else if (seqDelta > 1) {
          // resend request required as have missed messages.

          // We process a Logon beforehand to confirm the connection even we out of sync
          if (msgType === MsgType.Logon) {
            this.peerLogon(view)
          }
          // If the out of sync message is a resend request itself, then we handle it first in order
          // to avoid triggering an endless loop of both sides sending resend requests in response to resend requests.
          if (msgType === MsgType.ResendRequest) {
            this.onResendRequest(view)
          }
          this.sendResendRequest(lastSeq, seqNo)
        } else {
          ret = true
          state.lastPeerMsgSeqNum = seqNo
        }
        return ret
      }
    }
  }

  protected checkForwardMsg (msgType: string, view: MsgView): void {
    const okToForward = this.validStateApplicationMsg()
    if (okToForward) {
      this.sessionLogger.info(`ascii forwarding msgType = '${msgType}' to application`)
      this.setState(SessionState.ActiveNormalSession)
      this.onApplicationMsg(msgType, view)
    } else {
      this.terminate(new Error(`msgType ${msgType} received in state ${this.stateString()}`))
    }
  }

  private sendReject (msgType: string, seqNo: number, msg: string, reason: number): void {
    const factory = this.config.factory
    const reject = factory?.reject(msgType, seqNo, msg, reason)
    if (reject) {
      this.sessionLogger.warning(`rejecting with ${JSON.stringify(reject)}`)
      this.send(MsgType.Reject, reject)
    }
  }

  protected sendResendRequest (lastSeq: number, receivedSeq: number): void {
    const resend = this.config.factory?.resendRequest(lastSeq + 1, 0)
    if (resend) {
      this.sessionLogger.warning(`received seq ${receivedSeq}, but last known seq is ${lastSeq}. Sending resend request for all messages > ${lastSeq}`)
      this.send(MsgType.ResendRequest, resend)
    }
  }

  private checkIntegrity (msgType: string, view: MsgView): boolean {
    const state = this.sessionState
    const seqNum = view.getTyped(MsgTag.MsgSeqNum) as number

    const received: number = parseInt(view.getString(MsgTag.CheckSum) ?? '', 10)
    const computed = view.checksum()
    if (received !== computed) {
      const msg: string = `msgType ${msgType} checksum failed. received = ${received} computed = ${computed}`
      this.sendReject(msgType, seqNum, msg, SessionRejectReason.ValueIsIncorrect)
      return false
    }

    if (view.segment.type === SegmentType.Unknown) {
      const msg: string = `msgType ${msgType} unknown`
      this.sendReject(msgType, seqNum, msg, SessionRejectReason.InvalidMsgType)
      return false
    }

    const invalid = view.invalid()
    if (invalid.length > 0) {
      const msg: string = `msgType ${msgType} invalid tag${invalid.length > 1 ? 's' : ''} ${invalid.join(', ')}`
      this.sendReject(msgType, seqNum, msg, SessionRejectReason.InvalidTagNumber)
      return false
    }

    const undefinedMsg: string | null = view.undefinedForMsg()
    if (undefinedMsg) {
      const msg: string = `msgType ${msgType} ${undefinedMsg}`
      this.sendReject(msgType, seqNum, msg, SessionRejectReason.TagNotDefinedForThisMessageType)
      return false
    }

    const missingRequired = view.missing()
    if (missingRequired.length > 0) {
      const msg: string = `msgType ${msgType} missing required tag${missingRequired.length > 1 ? 's' : ''} ${missingRequired.join(', ')}`
      this.sendReject(msgType, seqNum, msg, SessionRejectReason.RequiredTagMissing)
      return false
    }

    switch (state.state) {
      case SessionState.InitiationLogonReceived:
      case SessionState.InitiationLogonResponse: {
        const targetCompId = view.getString(MsgTag.TargetCompID)
        if (targetCompId !== state.compId) {
          const msg: string = `msgType ${msgType} unexpected TargetCompID ${targetCompId} expecting ${state.compId})`
          this.sendReject(msgType, seqNum, msg, SessionRejectReason.CompIDProblem)
          return false
        }

        const peerCompId = view.getString(MsgTag.SenderCompID)
        if (peerCompId !== state.peerCompId) {
          const msg: string = `msgType ${msgType} unexpected SenderCompID ${peerCompId}  expecting ${state.compId}`
          this.sendReject(msgType, seqNum, msg, SessionRejectReason.CompIDProblem)
          return false
        }
      }
        break

      default: {
        break
      }
    }

    return true
  }

  /**
   * Override to resend stored messages following a sequence reset.
   * @protected
   */
  protected onResendRequest (view: MsgView): void {
    // if no records are in store then send a gap fill for entire sequence
    this.setState(SessionState.HandleResendRequest)
    const [beginSeqNo, requestedEndSeqNo] = view.getTypedTags([MsgTag.BeginSeqNo, MsgTag.EndSeqNo])
    const endSeqNo = requestedEndSeqNo === 0
      ? this.sessionState.lastSentSeqNum()
      : requestedEndSeqNo

    this.sessionLogger.info(`onResendRequest getResendRequest beginSeqNo = ${beginSeqNo}, endSeqNo = ${endSeqNo}`)
    this.resender.getResendRequest(beginSeqNo as number, endSeqNo as number).then((records: IFixMsgStoreRecord[]) => {
      const validRecords = records.filter(rec => rec.obj !== null)
      this.sessionLogger.info(`sending ${validRecords.length}`)
      validRecords.forEach(rec => {
        if (rec.obj) {
          this.send(rec.msgType, rec.obj)
        }
      })
      this.setState(SessionState.ActiveNormalSession)
    }).catch((e: Error) => {
      this.sessionLogger.error(e)
    })
  }

  okForLogon (): boolean {
    const state = this.sessionState.state
    if (this.acceptor) {
      return state === SessionState.WaitingForALogon
    }
    return state === SessionState.InitiationLogonSent
  }

  protected onSessionMsg (msgType: string, view: MsgView): void {
    const logger = this.sessionLogger

    switch (msgType) {
      case MsgType.Logon: {
        // only valid to receive a logon when in LogonSent or WaitingALogon
        // else will drop connection immediately.
        if (this.okForLogon()) {
          this.peerLogon(view)
        } else {
          this.terminate(new Error(`state ${this.stateString()} is illegal for Logon`))
        }
        break
      }

      case MsgType.Logout: {
        this.peerLogout(view)
        break
      }

      case MsgType.TestRequest: {
        const req: string | null = view.getString(MsgTag.TestReqID)
        if (req) {
          this.sendHeartbeat(req)
        }
        break
      }

      case MsgType.Heartbeat: {
        this.sessionState.lastTestRequestAt = null
        this.setState(SessionState.ActiveNormalSession)
        break
      }

      case MsgType.ResendRequest: {
        logger.info(`peer sends '${msgType}' resend request.`)
        this.onResendRequest(view)
        break
      }

      case MsgType.SequenceReset: {
        const newSeqNo: number = view.getTyped(MsgTag.NewSeqNo) as number
        logger.info(`peer sends '${msgType}' sequence reset. newSeqNo = ${newSeqNo}`)
        // expect  newSeqNo to be the next message's sequence number.
        this.sessionState.lastPeerMsgSeqNum = newSeqNo - 1
        break
      }

      case MsgType.Reject: {
        logger.info(`peer rejects type '${msgType}' with text '${view.getTyped(MsgTag.Text)}'`)
        break
      }
    }
  }

  protected onMsg (msgType: string, view: MsgView): void {
    if (!this.checkSeqNo(msgType, view)) {
      this.sessionLogger.info(`message '${msgType}' failed checkSeqNo.`)
      return
    }

    if (this.checkMsgIntegrity && !this.checkIntegrity(msgType, view)) {
      this.sessionLogger.info(`message '${msgType}' failed checkIntegrity.`)
      switch (msgType) {
        case MsgType.Logon: {
          this.setState(SessionState.PeerLogonRejected)
          this.startTimer()
          break
        }
      }
      return
    }

    switch (msgType) {
      case MsgType.Logon:
      case MsgType.Logout:
      case MsgType.TestRequest:
      case MsgType.Reject:
      case MsgType.SequenceReset:
      case MsgType.Heartbeat:
      case MsgType.ResendRequest: {
        this.onSessionMsg(msgType, view)
        break
      }

      default: {
        this.checkForwardMsg(msgType, view)
        break
      }
    }
  }

  private startTimer (interval: number = 200): void {
    const logger = this.sessionLogger
    logger.info(`start heartbeat timer. interval = ${interval}`)
    this.timer = setInterval(() => {
      this.tick()
    }, interval)
  }

  private peerLogon (view: MsgView): void {
    const logger = this.sessionLogger
    const [heartBtInt, peerCompId, userName, password] = view.getTypedTags([MsgTag.HeartBtInt, MsgTag.SenderCompID, MsgTag.Username, MsgTag.Password])
    logger.info(`peerLogon Username = ${userName}, heartBtInt = ${heartBtInt}, peerCompId = ${peerCompId}, userName = ${userName}`)
    const state = this.sessionState
    state.peerHeartBeatSecs = view.getTyped(MsgTag.HeartBtInt) as number
    state.peerCompId = view.getTyped(MsgTag.SenderCompID) as string
    const res = this.onLogon(view, userName as string, password as string)
    // currently not using this.
    logger.info(`peerLogon onLogon returns ${res}`)
    if (this.acceptor) {
      this.setState(SessionState.InitiationLogonResponse)
      logger.info('acceptor responds to logon request')
      this.sendLogon() // if res send response else reject, terminate
    } else { // as an initiator the acceptor has responded
      logger.info('initiator receives logon response')
      this.setState(SessionState.InitiationLogonReceived)
    }
    if (this.heartbeat) {
      this.startTimer()
    }
    logger.info('system ready, inform app')
    this.onReady(view)
  }

  private sendTestRequest (): void {
    const factory = this.config.factory
    this.setState(SessionState.AwaitingProcessingResponseToTestRequest)
    const tr = factory?.testRequest()
    if (tr) {
      this.send(MsgType.TestRequest, tr)
    }
  }

  private sendHeartbeat (testReqId: string): void {
    const factory = this.config.factory
    const hb = factory?.heartbeat(testReqId)
    if (hb) {
      this.send(MsgType.Heartbeat, hb)
    }
  }

  private tick (): void {
    if (!this.transport) return
    const sessionState = this.sessionState
    const action: TickAction = sessionState.calcAction(new Date())
    const application: IMsgApplication | null = this.transport.config.description.application ?? null
    const logger = this.sessionLogger

    switch (action) {
      case TickAction.Nothing: {
        // all is well
        break
      }

      case TickAction.TestRequest: {
        logger.debug(`send test req. state = ${sessionState.toString()}`)
        this.sendTestRequest()
        break
      }

      case TickAction.Heartbeat: {
        logger.debug(`send heartbeat. state = ${sessionState.toString()}`)
        this.sendHeartbeat(sessionState.now.toUTCString())
        break
      }

      case TickAction.TerminateOnError: {
        logger.info(sessionState.toString())
        this.terminate(new Error(`${application?.name}: peer not responding`))
        break
      }

      case TickAction.Stop: {
        logger.info(sessionState.toString())
        logger.info('stopping')
        this.stop()
        break
      }

      default:
        throw new Error('unexpected action')
    }
  }
}
