///|
pub(all) struct FileIssue {
  file : Int32
  path : String
  expected : Int64
  actual : Int64?
  reason : String
}

///|
pub(all) struct PieceFailure {
  scheme : String
  piece : Int32
  spans : Array[PieceSpan]
  reason : String
}

///|
pub(all) struct VerificationReport {
  ok : Bool
  scope : VerificationScope
  checked_pieces : Int32
  skipped_pieces : Int32
  failed_pieces : Int32
  unreadable_pieces : Int32
  file_issues : Array[FileIssue]
  failures : Array[PieceFailure]
  details_truncated : Bool
}

///|
priv struct VerifyState {
  mut checked : Int32
  mut skipped : Int32
  mut failed : Int32
  mut unreadable : Int32
  failures : Array[PieceFailure]
  limit : Int32
  control : VerifyControl?
}

///|
fn record_piece(
  state : VerifyState,
  scheme : String,
  index : Int32,
  spans : Array[PieceSpan],
  matches : Bool,
  readable : Bool,
) -> Unit raise CrateError {
  state.checked += 1
  if !matches || !readable {
    state.failed += 1
    if !readable {
      state.unreadable += 1
    }
    if state.failures.length() < state.limit {
      state.failures.push({
        scheme,
        piece: index,
        spans: spans.copy(),
        reason: if readable {
          "hash_mismatch"
        } else {
          "unreadable"
        },
      })
    }
  }
  if state.control is Some(control) {
    control.checked += 1
    if !matches || !readable {
      control.failed += 1
    }
    if !readable {
      control.unreadable += 1
    }
    emit_verification_progress(Some(control), "check")
  }
}

///|
fn verify_v1(
  parsed : ParsedTorrent,
  selected : Array[Bool],
  lengths : Array[Int64?],
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
  state : VerifyState,
) -> Unit raise CrateError {
  let piece_length = parsed.summary.piece_length
  let buffer : FixedArray[Byte] = FixedArray::make(piece_length, 0)
  let spans : Array[PieceSpan] = []
  let mut used = 0
  let mut piece = 0
  let mut readable = true
  for entry in parsed.entries {
    let mut offset = 0L
    while offset < entry.length {
      let remaining = entry.length - offset
      let size = if remaining < (piece_length - used).to_int64() {
        remaining.to_int()
      } else {
        piece_length - used
      }
      let size = if size > 1048576 { 1048576 } else { size }
      if selected[piece] {
        let valid_size = entry.padding ||
          lengths[entry.source] ==
          Some(parsed.summary.files[entry.source].length)
        let data = if entry.padding || !valid_size {
          Bytes::make(size, 0)
        } else {
          read_exact(read, entry.source, offset, size) catch {
            _ => {
              readable = false
              Bytes::make(size, 0)
            }
          }
        }
        check_verification_control(state.control)
        if !valid_size {
          readable = false
        }
        if !entry.padding {
          spans.push({ file: entry.source, offset, length: size, })
        }
        buffer.blit_from_bytes(used, data, 0, size)
      }
      used += size
      offset += size.to_int64()
      if used == piece_length {
        if selected[piece] {
          let actual = Bytes::from_array(@crypto.sha1(buffer)[:])
          record_piece(
            state,
            "sha1",
            piece,
            spans,
            actual == parsed.pieces[piece * 20:piece * 20 + 20].to_owned(),
            readable,
          )
        } else {
          state.skipped += 1
        }
        spans.clear()
        used = 0
        piece += 1
        readable = true
      }
    }
  }
  if used > 0 {
    if selected[piece] {
      let actual = Bytes::from_array(
        @crypto.sha1(Bytes::from_array(buffer[0:used]))[:],
      )
      record_piece(
        state,
        "sha1",
        piece,
        spans,
        actual == parsed.pieces[piece * 20:piece * 20 + 20].to_owned(),
        readable,
      )
    } else {
      state.skipped += 1
    }
  }
}

///|
fn verify_v2(
  parsed : ParsedTorrent,
  selected : Array[Bool],
  lengths : Array[Int64?],
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
  state : VerifyState,
) -> Unit raise CrateError {
  let piece = parsed.summary.piece_length
  for index, file in parsed.summary.files {
    if file.length == 0L {
      continue
    }
    let count = ((file.length + piece.to_int64() - 1L) / piece.to_int64()).to_int()
    if !selected[index] {
      state.skipped += count
      continue
    }
    for p in 0.. read(index, o, n)),
        ) catch {
          _ => None
        }
      } else {
        None
      }
      check_verification_control(state.control)
      let matches = match actual {
        None => false
        Some(actual) =>
          if count == 1 {
            actual == parsed.roots[index].root
          } else {
            actual ==
            parsed.roots[index].piece_layer[p * 32:p * 32 + 32].to_owned()
          }
      }
      record_piece(
        state,
        "sha256",
        p,
        [{ file: index, offset, length, }],
        matches,
        actual is Some(_),
      )
    }
  }
}

///|
/// Check real sizes and hashes. Missing files are None. Read errors cannot pass as zero data.
/// Failure detail storage is bounded; aggregate counters always include every piece.
pub fn verify_payload(
  torrent : Bytes,
  lengths : Array[Int64?],
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
  detail_limit~ : Int32 = 4096,
  selected_files~ : Array[Int32]? = None,
) -> VerificationReport raise CrateError {
  let parsed = parse_torrent(torrent)
  verify_parsed(parsed, lengths, read, detail_limit, selected_files, None)
}

///|
fn verify_parsed(
  parsed : ParsedTorrent,
  lengths : Array[Int64?],
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
  detail_limit : Int32,
  selected_files : Array[Int32]?,
  on_progress : ((VerificationProgress) -> Bool raise CrateError)?,
) -> VerificationReport raise CrateError {
  let plan = plan_verification(parsed, selected_files)
  if lengths.length() != parsed.summary.files.length() ||
    detail_limit < 1 ||
    detail_limit > 10000 {
    reject("verify", "file size mapping or detail limit is invalid")
  }
  let issues : Array[FileIssue] = []
  for i in plan.scope.required_files {
    let file = parsed.summary.files[i]
    if lengths[i] != Some(file.length) {
      issues.push({
        file: i,
        path: file.path.join("/"),
        expected: file.length,
        actual: lengths[i],
        reason: if lengths[i] is None {
          "missing"
        } else {
          "size_mismatch"
        },
      })
    }
  }
  let control = on_progress.map(fn(callback) {
    new_verify_control(parsed, plan, callback)
  })
  let observed_read = match control {
    None => read
    Some(control) =>
      fn(i, o, n) raise CrateError { controlled_read(control, read, i, o, n) }
  }
  emit_verification_progress(control, "start")
  let state : VerifyState = {
    checked: 0,
    skipped: 0,
    failed: 0,
    unreadable: 0,
    failures: [],
    limit: detail_limit,
    control,
  }
  match parsed.summary.mode {
    V1 => verify_v1(parsed, plan.v1_pieces, lengths, observed_read, state)
    V2 => verify_v2(parsed, plan.selected, lengths, observed_read, state)
    Hybrid =>
      verify_hybrid(parsed, plan.selected, lengths, observed_read, state)
  }
  emit_verification_progress(control, "finalize")
  {
    ok: state.failed == 0 && issues.is_empty(),
    scope: plan.scope,
    checked_pieces: state.checked,
    skipped_pieces: state.skipped,
    failed_pieces: state.failed,
    unreadable_pieces: state.unreadable,
    file_issues: issues,
    failures: state.failures,
    details_truncated: state.failed > state.failures.length(),
  }
}