import { MerklePath } from '@bsv/sdk'
import {
  arraysEqual,
  asString,
  sdk,
  StorageProvider,
  TableProvenTxReq,
  TableProvenTxReqDynamics,
  verifyId,
  verifyOne,
  verifyOneOrNone,
  verifyTruthy,
  WalletStorageManager
} from '../../../index.client'
import { EntityBase, EntityStorage, SyncMap } from '.'

import { StorageProcessActionArgs } from '../../../sdk'

export class EntityProvenTxReq extends EntityBase<TableProvenTxReq> {
  static async fromStorageTxid(
    storage: EntityStorage,
    txid: string,
    trx?: sdk.TrxToken
  ): Promise<EntityProvenTxReq | undefined> {
    const reqApi = verifyOneOrNone(await storage.findProvenTxReqs({ partial: { txid }, trx }))
    if (!reqApi) return undefined
    return new EntityProvenTxReq(reqApi)
  }

  static async fromStorageId(storage: EntityStorage, id: number, trx?: sdk.TrxToken): Promise<EntityProvenTxReq> {
    const reqApi = verifyOneOrNone(await storage.findProvenTxReqs({ partial: { provenTxReqId: id }, trx }))
    if (!reqApi) throw new sdk.WERR_INTERNAL(`proven_tx_reqs with id ${id} is missing.`)
    return new EntityProvenTxReq(reqApi)
  }

  static fromTxid(txid: string, rawTx: number[], inputBEEF?: number[]): EntityProvenTxReq {
    const now = new Date()
    return new EntityProvenTxReq({
      provenTxReqId: 0,
      created_at: now,
      updated_at: now,
      txid,
      inputBEEF,
      rawTx,
      status: 'unknown',
      history: '{}',
      notify: '{}',
      attempts: 0,
      notified: false
    })
  }

  history: ProvenTxReqHistory
  notify: ProvenTxReqNotify

  packApiHistory() {
    this.api.history = JSON.stringify(this.history)
  }
  packApiNotify() {
    this.api.notify = JSON.stringify(this.notify)
  }

  unpackApiHistory() {
    this.history = JSON.parse(this.api.history)
  }
  unpackApiNotify() {
    this.notify = JSON.parse(this.api.notify)
  }

  get apiHistory(): string {
    this.packApiHistory()
    return this.api.history
  }
  get apiNotify(): string {
    this.packApiNotify()
    return this.api.notify
  }

  set apiHistory(v: string) {
    this.api.history = v
    this.unpackApiHistory()
  }
  set apiNotify(v: string) {
    this.api.notify = v
    this.unpackApiNotify()
  }

  updateApi(): void {
    this.packApiHistory()
    this.packApiNotify()
  }

  unpackApi(): void {
    this.unpackApiHistory()
    this.unpackApiNotify()
    if (this.notify.transactionIds) {
      // Cleanup null values and duplicates.
      const transactionIds: number[] = []
      for (const id of this.notify.transactionIds) {
        if (Number.isInteger(id) && !transactionIds.some(txid => txid === id)) transactionIds.push(id)
      }
      this.notify.transactionIds = transactionIds
    }
  }

  async refreshFromStorage(storage: EntityStorage | WalletStorageManager, trx?: sdk.TrxToken): Promise<void> {
    const newApi = verifyOne(await storage.findProvenTxReqs({ partial: { provenTxReqId: this.id }, trx }))
    this.api = newApi
    this.unpackApi()
  }

  constructor(api?: TableProvenTxReq) {
    const now = new Date()
    super(
      api || {
        provenTxReqId: 0,
        created_at: now,
        updated_at: now,
        txid: '',
        rawTx: [],
        history: '',
        notify: '',
        attempts: 0,
        status: 'unknown',
        notified: false
      }
    )
    this.history = {}
    this.notify = {}
    this.unpackApi()
  }

  /**
   * Returns history to only what followed since date.
   */
  historySince(since: Date): ProvenTxReqHistory {
    const fh: ProvenTxReqHistory = { notes: [] }
    const filter = since.toISOString()
    const notes = this.history.notes
    if (notes && fh.notes) {
      for (const note of notes) if (note.when && note.when > filter) fh.notes.push(note)
    }
    return fh
  }

  historyPretty(since?: Date, indent = 0): string {
    const h = since ? this.historySince(since) : { ...this.history }
    if (!h.notes) return ''
    const whenLimit = since ? since.toISOString() : undefined
    let log = ''
    for (const note of h.notes) {
      if (whenLimit && note.when && note.when < whenLimit) continue
      log += this.prettyNote(note) + '\n'
    }
    return log
  }

  prettyNote(note: sdk.ReqHistoryNote): string {
    let log = `${note.when}: ${note.what}`
    for (const [key, val] of Object.entries(note)) {
      if (key !== 'when' && key !== 'what') {
        if (typeof val === 'string') log += ' ' + key + ':`' + val + '`'
        else log += ' ' + key + ':' + val
      }
    }
    return log
  }

  getHistorySummary(): ProvenTxReqHistorySummaryApi {
    const summary: ProvenTxReqHistorySummaryApi = {
      setToCompleted: false,
      setToUnmined: false,
      setToCallback: false,
      setToDoubleSpend: false,
      setToSending: false,
      setToUnconfirmed: false
    }
    const h = this.history
    if (h.notes) {
      for (const note of h.notes) {
        this.parseHistoryNote(note, summary)
      }
    }
    return summary
  }

  parseHistoryNote(note: sdk.ReqHistoryNote, summary?: ProvenTxReqHistorySummaryApi): string {
    const c = summary || {
      setToCompleted: false,
      setToUnmined: false,
      setToCallback: false,
      setToDoubleSpend: false,
      setToSending: false,
      setToUnconfirmed: false
    }
    let n = this.prettyNote(note)
    try {
      switch (note.what) {
        case 'status':
          {
            const status = <sdk.ProvenTxReqStatus>note.status_now
            switch (status) {
              case 'completed':
                c.setToCompleted = true
                break
              case 'unmined':
                c.setToUnmined = true
                break
              case 'callback':
                c.setToCallback = true
                break
              case 'doubleSpend':
                c.setToDoubleSpend = true
                break
              case 'sending':
                c.setToSending = true
                break
              case 'unconfirmed':
                c.setToUnconfirmed = true
                break
              default:
                break
            }
          }
          break
        default:
          break
      }
    } catch {
      /** */
    }
    return n
  }

  addNotifyTransactionId(id: number) {
    if (!Number.isInteger(id)) throw new sdk.WERR_INVALID_PARAMETER('id', 'integer')
    const s = new Set(this.notify.transactionIds || [])
    s.add(id)
    this.notify.transactionIds = [...s].sort((a, b) => (a > b ? 1 : a < b ? -1 : 0))
    this.notified = false
  }

  /**
   * Adds a note to history.
   * Notes with identical property values to an existing note are ignored.
   * @param note Note to add
   * @param noDupes if true, only newest note with same `what` value is retained.
   */
  addHistoryNote(note: sdk.ReqHistoryNote, noDupes?: boolean) {
    if (!this.history.notes) this.history.notes = []
    if (!note.when) note.when = new Date().toISOString()
    if (noDupes) {
      // Remove any existing notes with same 'what' value and either no 'when' or an earlier 'when'
      this.history.notes = this.history.notes!.filter(n => n.what !== note.what || (n.when && n.when > note.when!))
    }
    let addNote = true
    for (const n of this.history.notes) {
      let isEqual = true
      for (const [k, v] of Object.entries(n)) {
        if (v !== note[k]) {
          isEqual = false
          break
        }
      }
      if (isEqual) addNote = false
      if (!addNote) break
    }
    if (addNote) {
      this.history.notes.push(note as sdk.ReqHistoryNote)
      const k = (n: sdk.ReqHistoryNote): string => {
        return `${n.when} ${n.what}`
      }
      this.history.notes.sort((a, b) => (k(a) < k(b) ? -1 : k(a) > k(b) ? 1 : 0))
    }
  }

  /**
     * Updates database record with current state of this EntityUser

     * @param storage 
     * @param trx 
     */
  async updateStorage(storage: EntityStorage, trx?: sdk.TrxToken) {
    this.updated_at = new Date()
    this.updateApi()
    if (this.id === 0) {
      await storage.insertProvenTxReq(this.api)
    }
    const update: Partial<TableProvenTxReq> = { ...this.api }
    await storage.updateProvenTxReq(this.id, update, trx)
  }

  /**
   * Update storage with changes to non-static properties:
   *   updated_at
   *   provenTxId
   *   status
   *   history
   *   notify
   *   notified
   *   attempts
   *   batch
   *
   * @param storage
   * @param trx
   */
  async updateStorageDynamicProperties(storage: WalletStorageManager | StorageProvider, trx?: sdk.TrxToken) {
    this.updated_at = new Date()
    this.updateApi()
    const update: Partial<TableProvenTxReqDynamics> = {
      updated_at: this.api.updated_at,
      provenTxId: this.api.provenTxId,
      status: this.api.status,
      history: this.api.history,
      notify: this.api.notify,
      notified: this.api.notified,
      attempts: this.api.attempts,
      batch: this.api.batch
    }
    if (storage.isStorageProvider()) {
      const sp = storage as StorageProvider
      await sp.updateProvenTxReqDynamics(this.id, update, trx)
    } else {
      const wsm = storage as WalletStorageManager
      await wsm.runAsStorageProvider(async sp => {
        await sp.updateProvenTxReqDynamics(this.id, update, trx)
      })
    }
  }

  async insertOrMerge(storage: EntityStorage, trx?: sdk.TrxToken): Promise<EntityProvenTxReq> {
    const req = await storage.transaction<EntityProvenTxReq>(async trx => {
      let reqApi0 = this.toApi()
      const { req: reqApi1, isNew } = await storage.findOrInsertProvenTxReq(reqApi0, trx)
      if (isNew) {
        return new EntityProvenTxReq(reqApi1)
      } else {
        const req = new EntityProvenTxReq(reqApi1)
        req.mergeNotifyTransactionIds(reqApi0)
        req.mergeHistory(reqApi0, undefined, true)
        await req.updateStorage(storage, trx)
        return req
      }
    }, trx)
    return req
  }

  /**
   * See `ProvenTxReqStatusApi`
   */
  get status() {
    return this.api.status
  }
  set status(v: sdk.ProvenTxReqStatus) {
    if (v !== this.api.status) {
      this.addHistoryNote({ what: 'status', status_was: this.api.status, status_now: v })
      this.api.status = v
    }
  }
  get provenTxReqId() {
    return this.api.provenTxReqId
  }
  set provenTxReqId(v: number) {
    this.api.provenTxReqId = v
  }
  get created_at() {
    return this.api.created_at
  }
  set created_at(v: Date) {
    this.api.created_at = v
  }
  get updated_at() {
    return this.api.updated_at
  }
  set updated_at(v: Date) {
    this.api.updated_at = v
  }
  get txid() {
    return this.api.txid
  }
  set txid(v: string) {
    this.api.txid = v
  }
  get inputBEEF() {
    return this.api.inputBEEF
  }
  set inputBEEF(v: number[] | undefined) {
    this.api.inputBEEF = v
  }
  get rawTx() {
    return this.api.rawTx
  }
  set rawTx(v: number[]) {
    this.api.rawTx = v
  }
  get attempts() {
    return this.api.attempts
  }
  set attempts(v: number) {
    this.api.attempts = v
  }
  get provenTxId() {
    return this.api.provenTxId
  }
  set provenTxId(v: number | undefined) {
    this.api.provenTxId = v
  }
  get notified() {
    return this.api.notified
  }
  set notified(v: boolean) {
    this.api.notified = v
  }
  get batch() {
    return this.api.batch
  }
  set batch(v: string | undefined) {
    this.api.batch = v
  }

  override get id() {
    return this.api.provenTxReqId
  }
  override set id(v: number) {
    this.api.provenTxReqId = v
  }
  override get entityName(): string {
    return 'provenTxReq'
  }
  override get entityTable(): string {
    return 'proven_tx_reqs'
  }

  /**
   * 'convergent' equality must satisfy (A sync B) equals (B sync A)
   */
  override equals(ei: TableProvenTxReq, syncMap?: SyncMap | undefined): boolean {
    const eo = this.toApi()
    if (
      eo.txid != ei.txid ||
      !arraysEqual(eo.rawTx, ei.rawTx) ||
      (!eo.inputBEEF && ei.inputBEEF) ||
      (eo.inputBEEF && !ei.inputBEEF) ||
      (eo.inputBEEF && ei.inputBEEF && !arraysEqual(eo.inputBEEF, ei.inputBEEF)) ||
      eo.batch != ei.batch
    )
      return false
    if (syncMap) {
      if (
        // attempts doesn't matter for convergent equality
        // history doesn't matter for convergent equality
        // only local transactionIds matter, that cared about this txid in sorted order
        eo.provenTxReqId !== syncMap.provenTxReq.idMap[verifyId(ei.provenTxReqId)] ||
        (!eo.provenTxId && ei.provenTxId) ||
        (eo.provenTxId && !ei.provenTxId) ||
        (ei.provenTxId && eo.provenTxId !== syncMap.provenTx.idMap[ei.provenTxId])
        // || eo.created_at !== minDate(ei.created_at, eo.created_at)
        // || eo.updated_at !== maxDate(ei.updated_at, eo.updated_at)
      )
        return false
    } else {
      if (
        eo.attempts != ei.attempts ||
        eo.history != ei.history ||
        eo.notify != ei.notify ||
        eo.provenTxReqId !== ei.provenTxReqId ||
        eo.provenTxId !== ei.provenTxId
        // || eo.created_at !== ei.created_at
        // || eo.updated_at !== ei.updated_at
      )
        return false
    }
    return true
  }

  static async mergeFind(
    storage: EntityStorage,
    userId: number,
    ei: TableProvenTxReq,
    syncMap: SyncMap,
    trx?: sdk.TrxToken
  ): Promise<{ found: boolean; eo: EntityProvenTxReq; eiId: number }> {
    const ef = verifyOneOrNone(await storage.findProvenTxReqs({ partial: { txid: ei.txid }, trx }))
    return {
      found: !!ef,
      eo: new EntityProvenTxReq(ef || { ...ei }),
      eiId: verifyId(ei.provenTxReqId)
    }
  }

  mapNotifyTransactionIds(syncMap: SyncMap): void {
    // Map external notification transaction ids to local ids
    const externalIds = this.notify.transactionIds || []
    this.notify.transactionIds = []
    for (const transactionId of externalIds) {
      const localTxId: number | undefined = syncMap.transaction.idMap[transactionId]
      if (localTxId) {
        this.addNotifyTransactionId(localTxId)
      }
    }
  }

  mergeNotifyTransactionIds(ei: TableProvenTxReq, syncMap?: SyncMap): void {
    // Map external notification transaction ids to local ids and merge them if they exist.
    const eie = new EntityProvenTxReq(ei)
    if (eie.notify.transactionIds) {
      this.notify.transactionIds ||= []
      for (const transactionId of eie.notify.transactionIds) {
        const localTxId: number | undefined = syncMap ? syncMap.transaction.idMap[transactionId] : transactionId
        if (localTxId) {
          this.addNotifyTransactionId(localTxId)
        }
      }
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  mergeHistory(ei: TableProvenTxReq, syncMap?: SyncMap, noDupes?: boolean): void {
    const eie = new EntityProvenTxReq(ei)
    if (eie.history.notes) {
      for (const note of eie.history.notes) {
        this.addHistoryNote(note)
      }
    }
  }

  static isTerminalStatus(status: sdk.ProvenTxReqStatus): boolean {
    return sdk.ProvenTxReqTerminalStatus.some(s => s === status)
  }

  override async mergeNew(storage: EntityStorage, userId: number, syncMap: SyncMap, trx?: sdk.TrxToken): Promise<void> {
    if (this.provenTxId) this.provenTxId = syncMap.provenTx.idMap[this.provenTxId]
    this.mapNotifyTransactionIds(syncMap)
    this.provenTxReqId = 0
    this.provenTxReqId = await storage.insertProvenTxReq(this.toApi(), trx)
  }

  /**
   * When merging `ProvenTxReq`, care is taken to avoid short-cirtuiting notification: `status` must not transition to `completed` without
   * passing through `notifying`. Thus a full convergent merge passes through these sequence steps:
   * 1. Remote storage completes before local storage.
   * 2. The remotely completed req and ProvenTx sync to local storage.
   * 3. The local storage transitions to `notifying`, after merging the remote attempts and history.
   * 4. The local storage notifies, transitioning to `completed`.
   * 5. Having been updated, the local req, but not ProvenTx sync to remote storage, but do not merge because the earlier `completed` wins.
   * 6. Convergent equality is achieved (completing work - history and attempts are equal)
   *
   * On terminal failure: `doubleSpend` trumps `invalid` as it contains more data.
   */
  override async mergeExisting(
    storage: EntityStorage,
    since: Date | undefined,
    ei: TableProvenTxReq,
    syncMap: SyncMap,
    trx?: sdk.TrxToken
  ): Promise<boolean> {
    if (!this.batch && ei.batch) this.batch = ei.batch
    else if (this.batch && ei.batch && this.batch !== ei.batch)
      throw new sdk.WERR_INTERNAL('ProvenTxReq merge batch not equal.')

    this.mergeHistory(ei, syncMap, true)
    this.mergeNotifyTransactionIds(ei, syncMap)

    this.updated_at = new Date(Math.max(ei.updated_at.getTime(), this.updated_at.getTime()))
    await storage.updateProvenTxReq(this.id, this.toApi(), trx)
    return false
  }
}

export interface ProvenTxReqHistorySummaryApi {
  setToCompleted: boolean
  setToCallback: boolean
  setToUnmined: boolean
  setToDoubleSpend: boolean
  setToSending: boolean
  setToUnconfirmed: boolean
}

export interface ProvenTxReqHistory {
  /**
   * Keys are Date().toISOString()
   * Values are a description of what happened.
   */
  notes?: sdk.ReqHistoryNote[]
}

export interface ProvenTxReqNotify {
  transactionIds?: number[]
}
