///|
/// Immutable, unsigned snapshot of one verification and its exact metainfo input.
/// Constructed by verify_with_receipt, rather than by pairing an arbitrary report and subject.
pub struct VerificationReceipt {
  priv text : String
  priv diagnostics_text : String
}

///|
/// Verify and capture identity, real-file mapping, observed sizes and actual scope together.
/// The caller supplies stable contents and sizes; no clock, filesystem or authentication is implied.
pub fn verify_with_receipt(
  torrent : Bytes,
  lengths : Array[Int64?],
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
  detail_limit~ : Int32 = 4096,
  selected_files~ : Array[Int32]? = None,
) -> (VerificationReport, VerificationReceipt) raise CrateError {
  verify_receipt_impl(
    torrent,
    lengths,
    read,
    None,
    detail_limit,
    selected_files,
  )
}

///|
/// Cooperatively verify with synchronous progress and cancellation checkpoints.
/// False raises CrateError("cancelled", ...); observer errors propagate unchanged.
/// Cancellation returns neither a report nor a receipt, including at finalization.
pub fn verify_with_control(
  torrent : Bytes,
  lengths : Array[Int64?],
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
  on_progress : (VerificationProgress) -> Bool raise CrateError,
  detail_limit~ : Int32 = 4096,
  selected_files~ : Array[Int32]? = None,
) -> (VerificationReport, VerificationReceipt) raise CrateError {
  verify_receipt_impl(
    torrent,
    lengths,
    read,
    Some(on_progress),
    detail_limit,
    selected_files,
  )
}

///|
fn verify_receipt_impl(
  torrent : Bytes,
  lengths : Array[Int64?],
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
  on_progress : ((VerificationProgress) -> Bool raise CrateError)?,
  detail_limit : Int32,
  selected_files : Array[Int32]?,
) -> (VerificationReport, VerificationReceipt) raise CrateError {
  let parsed = parse_torrent(torrent)
  // Capture raw identity and caller-owned sizes before invoking any read callback.
  let metainfo_sha256 = @crypto.bytes_to_hex_string(@crypto.sha256(torrent))
  let metainfo_bytes = torrent.length().to_string()
  let sizes = lengths.copy()
  let report = verify_parsed(
    parsed, sizes, read, detail_limit, selected_files, on_progress,
  )
  let info = parsed.summary
  let diagnostics = file_diagnostics_json(
    info, report, metainfo_sha256, metainfo_bytes,
  )
  let subject = j_object([
    ("metainfo_sha256", Json::string(metainfo_sha256)),
    ("metainfo_bytes", Json::string(metainfo_bytes)),
    ("info_hash_v1", optional_text(info.info_hash_v1)),
    ("info_hash_v2", optional_text(info.info_hash_v2)),
    ("name", Json::string(info.name)),
    ("mode", Json::string(mode_name(info.mode))),
    ("piece_length", Json::number(info.piece_length.to_double())),
    ("single_file", Json::boolean(info.single_file)),
    (
      "files",
      Json::array(
        info.files.mapi(fn(i, file) {
          j_object([
            ("index", Json::number(i.to_double())),
            ("path", Json::array(file.path.map(Json::string))),
            ("length", Json::string(file.length.to_string())),
          ])
        }),
      ),
    ),
  ])
  let observations = report.scope.required_files.map(fn(i) {
    j_object([
      ("file", Json::number(i.to_double())),
      ("actual_bytes", optional_text(sizes[i].map(fn(n) { n.to_string() }))),
    ])
  })
  let result = j_object([
    ("schema", Json::string("swarmcrate.receipt.v1")),
    (
      "producer",
      j_object([
        ("name", Json::string("SwarmCrate")),
        ("version", Json::string("0.14.0")),
      ]),
    ),
    ("subject", subject),
    ("observations", Json::array(observations)),
    ("verification", verification_json(report)),
    ("file_diagnostics", diagnostics),
    (
      "outcome",
      Json::string(
        if !report.ok {
          "FAIL"
        } else if report.scope.complete {
          "PASS"
        } else {
          "PARTIAL_PASS"
        },
      ),
    ),
    ("is_digital_signature", Json::boolean(false)),
    ("publisher_authenticated", Json::boolean(false)),
  ])
  // Snapshot before returning the mutable report arrays to the consumer.
  (
    report,
    VerificationReceipt::{
      text: result.stringify(indent=2),
      diagnostics_text: diagnostics.stringify(indent=2),
    },
  )
}

///|
/// Serialize the immutable receipt snapshot. This does not reverify a saved JSON document.
pub fn receipt_json_text(receipt : VerificationReceipt) -> String {
  receipt.text
}

///|
/// Export immutable per-file candidate ranges bound to the same receipt and scope.
/// Truncated failure details make the range inventory incomplete, never a complete repair plan.
pub fn receipt_diagnostics_json_text(receipt : VerificationReceipt) -> String {
  receipt.diagnostics_text
}