# nimbus-eth1
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
#  * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
#    http://www.apache.org/licenses/LICENSE-2.0)
#  * MIT license ([LICENSE-MIT](LICENSE-MIT) or
#    http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed
# except according to those terms.

{.push raises: [].}

import
  std/[algorithm, sequtils, sets, tables],
  eth/common,
  results,
  ../aristo_journal/journal_scheduler,
  ../aristo_walk/persistent,
  ".."/[aristo_desc, aristo_blobify]

const
  ExtraDebugMessages = false

type
  JrnRec = tuple
    src: Hash256
    trg: Hash256
    size: int

when ExtraDebugMessages:
  import
    ../aristo_debug

# ------------------------------------------------------------------------------
# Private functions and helpers
# ------------------------------------------------------------------------------

template noValueError(info: static[string]; code: untyped) =
  try:
    code
  except ValueError as e:
    raiseAssert info & ", name=\"" & $e.name & "\", msg=\"" & e.msg & "\""

when ExtraDebugMessages:
  proc pp(t: var Table[QueueID,JrnRec]): string =
    result = "{"
    for qid in t.keys.toSeq.sorted:
      t.withValue(qid,w):
        result &= qid.pp & "#" & $w[].size & ","
    if result[^1] == '{':
      result &= "}"
    else:
      result[^1] = '}'

  proc pp(t: seq[QueueID]): string =
    result = "{"
    var list = t
    for n in 2 ..< list.len:
      if list[n-1] == list[n] - 1 and
         (list[n-2] == QueueID(0) or list[n-2] == list[n] - 2):
        list[n-1] = QueueID(0)
    for w in list:
      if w != QueueID(0):
        result &= w.pp & ","
      elif result[^1] == ',':
        result[^1] = '.'
        result &= "."
    if result[^1] == '{':
      result &= "}"
    else:
      result[^1] = '}'

  proc pp(t: HashSet[QueueID]): string =
    result = "{"
    var list = t.toSeq.sorted
    for n in 2 ..< list.len:
      if list[n-1] == list[n] - 1 and
         (list[n-2] == QueueID(0) or list[n-2] == list[n] - 2):
        list[n-1] = QueueID(0)
    for w in list:
      if w != QueueID(0):
        result &= w.pp & ","
      elif result[^1] == ',':
        result[^1] = '.'
        result &= "."
    if result[^1] == '{':
      result &= "}"
    else:
      result[^1] = '}'

# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------

proc checkJournal*[T: RdbBackendRef|MemBackendRef](
    _: type T;
    db: AristoDbRef;
      ): Result[void,(QueueID,AristoError)] =
  let jrn = db.backend.journal
  if jrn.isNil: return ok()

  var
    nToQid: seq[QueueID]             # qids sorted by history/age
    cached: HashSet[QueueID]         # `nToQid[]` as set
    saved: Table[QueueID,JrnRec]
    error: (QueueID,AristoError)

  when ExtraDebugMessages:
    var
      sizeTally = 0
      maxBlock = 0

    proc moan(n = -1, s = "", listOk = true) =
      var txt = ""
      if 0 <= n:
        txt &= " (" & $n & ")"
      if error[1] != AristoError(0):
        txt &= " oops"
      txt &=
        " jLen=" & $jrn.len &
        " tally=" & $sizeTally &
        " maxBlock=" & $maxBlock &
        ""
      if 0 < s.len:
        txt &= " " & s
      if error[1] != AristoError(0):
        txt &=
          " errQid=" &  error[0].pp &
          " error=" &  $error[1] &
          ""
      if listOk:
        txt &=
          "\n    cached=" &  cached.pp &
          "\n    saved=" & saved.pp &
          ""
      debugEcho "*** checkJournal", txt
  else:
    template moan(n = -1, s = "", listOk = true) =
      discard

  # Collect cached handles
  for n in 0 ..< jrn.len:
    let qid = jrn[n]
    # Must be no overlap
    if qid in cached:
      error = (qid,CheckJrnCachedQidOverlap)
      moan(2)
      return err(error)
    cached.incl qid
    nToQid.add qid

  # Collect saved data
  for (qid,fil) in db.backend.T.walkFilBe():
    var jrnRec: JrnRec
    jrnRec.src = fil.src
    jrnRec.trg = fil.trg

    when ExtraDebugMessages:
      let rc = fil.blobify
      if rc.isErr:
        moan(5)
        return err((qid,rc.error))
      jrnRec.size = rc.value.len
      if maxBlock < jrnRec.size:
        maxBlock = jrnRec.size
      sizeTally += jrnRec.size

    saved[qid] = jrnRec

  # Compare cached against saved data
  let
    savedQids = saved.keys.toSeq.toHashSet
    unsavedQids = cached - savedQids
    staleQids = savedQids - cached

  if 0 < unsavedQids.len:
    error = (unsavedQids.toSeq.sorted[0],CheckJrnSavedQidMissing)
    moan(6)
    return err(error)

  if 0 < staleQids.len:
    error = (staleQids.toSeq.sorted[0], CheckJrnSavedQidStale)
    moan(7)
    return err(error)

  # Compare whether journal records link together
  if 1 < nToQid.len:
    noValueError("linked journal records"):
      var prvRec = saved[nToQid[0]]
      for n in 1 ..< nToQid.len:
        let thisRec = saved[nToQid[n]]
        if prvRec.trg != thisRec.src:
          error = (nToQid[n],CheckJrnLinkingGap)
          moan(8, "qidInx=" & $n)
          return err(error)
        prvRec = thisRec

  moan(9, listOk=false)
  ok()

# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------