///|
fn make_plan(pkg : Package) -> RewritePlan {
  let report = pkg.audit
  let ops : Array[Operation] = []
  for f in report.findings {
    let rel = report.relationships.filter(fn(r) {
        r.kind == vba_rel && r.resolved == f.part
      })[0]
    ops.push({
      kind: "RemovePart",
      part: f.part,
      relationship_id: "",
      rule: "vba-v1",
      reason: "Remove VBA payload; macro functionality is lost.",
    })
    ops.push({
      kind: "RemoveRelationship",
      part: rel.source,
      relationship_id: rel.id,
      rule: "vba-v1",
      reason: "VBA is an implicit workbook relationship; no supported source XML references it.",
    })
    ops.push({
      kind: "RewriteRelationshipPart",
      part: relationship_part(rel.source),
      relationship_id: rel.id,
      rule: "vba-v1",
      reason: "Regenerate validated relationship metadata without VBA relation.",
    })
  }
  if report.format == "XLSM" {
    ops.push({
      kind: "ChangeMainContentType",
      part: report.main_part,
      relationship_id: "",
      rule: "format-v1",
      reason: "Convert macro-enabled workbook to ordinary XLSX, including macro-enabled files with no VBA.",
    })
    ops.push({
      kind: "RewriteContentTypes",
      part: "[Content_Types].xml",
      relationship_id: "",
      rule: "format-v1",
      reason: "Remove VBA mappings and change main type; retain other mappings.",
    })
  }
  {
    schema: "partsieve.plan.spike-v1",
    input_hash: report.input_hash,
    policy: "passive-office-v1",
    profile: report.profile,
    operations: ops,
  }
}

///|
pub fn plan(
  input : Bytes,
  limits? : Limits = Limits::default(),
) -> RewritePlan raise {
  make_plan(load_package(input, limits))
}

///|
fn write_relationships(pkg : Package, source : String) -> Bytes raise {
  let writer = @xml.Writer::new()
  writer.start_element("Relationships", [("xmlns", rel_ns)])
  for r in pkg.audit.relationships {
    if r.source != source || r.kind == vba_rel {
      continue
    }
    writer.empty_element("Relationship", [
      ("Id", r.id),
      ("Type", r.kind),
      ("Target", r.target),
    ])
  }
  writer.end_element("Relationships")
  @utf8.encode(writer.to_string()[:])
}

///|
fn write_content_types(pkg : Package) -> Bytes raise {
  let writer = @xml.Writer::new()
  writer.start_element("Types", [("xmlns", ct_ns)])
  let extensions = pkg.defaults.keys().collect()
  extensions.sort()
  for ext in extensions {
    let ct = pkg.defaults[ext]
    if ct != vba_ct {
      writer.empty_element("Default", [("Extension", ext), ("ContentType", ct)])
    }
  }
  let names = pkg.overrides.keys().collect()
  names.sort()
  for name in names {
    let ct = pkg.overrides[name]
    if ct == vba_ct {
      continue
    }
    writer.empty_element("Override", [
      ("PartName", "/" + name),
      ("ContentType", if name == pkg.audit.main_part { xlsx_ct } else { ct }),
    ])
  }
  // The workbook main type can legally come from a default mapping. Convert
  // that case using an explicit override while keeping other XML mappings.
  if !pkg.overrides.contains(pkg.audit.main_part) {
    writer.empty_element("Override", [
      ("PartName", "/" + pkg.audit.main_part),
      ("ContentType", xlsx_ct),
    ])
  }
  writer.end_element("Types")
  @utf8.encode(writer.to_string()[:])
}

///|
fn execute(pkg : Package, limits : Limits) -> Bytes raise {
  let archive = pkg.archive.clone()
  for f in pkg.audit.findings {
    guard archive.remove(f.part[:]) else {
      raise Refused("plan RemovePart precondition failed")
    }
  }
  if pkg.audit.findings.length() > 0 {
    let name = relationship_part(pkg.audit.main_part)
    guard archive.replace(
      name[:],
      write_relationships(pkg, pkg.audit.main_part)[:],
    ) else {
      raise Refused("plan relationship precondition failed")
    }
  }
  if pkg.audit.format == "XLSM" {
    guard archive.replace("[Content_Types].xml", write_content_types(pkg)[:]) else {
      raise Refused("content types precondition failed")
    }
  }
  @zip.write_preserving_limited(archive, max_output_bytes=limits.output_bytes) catch {
    @zip.ZipError(_, message) =>
      raise Incomplete("bounded output writing did not complete: " + message)
  }
}

///|
fn verify_parts(
  before : Package,
  after : Package,
  expected : Package,
) -> Array[Disposition] raise {
  let dispositions : Array[Disposition] = []
  guard after.audit.format == "XLSX" && after.audit.findings.length() == 0 else {
    raise Refused("output has forbidden VBA or macro-enabled type")
  }
  guard after.audit.parts == expected.audit.parts &&
    after.audit.relationships == expected.audit.relationships else {
    raise Refused("unexpected output part/relationship changes")
  }
  for p in before.audit.parts {
    let matches = after.audit.parts.filter(fn(q) { q.name == p.name })
    let out = if matches.length() == 0 { None } else { Some(matches[0]) }
    let removed = before.audit.findings.any(fn(f) { f.part == p.name })
    let metadata = before.audit.format == "XLSM" &&
      (
        p.name == "[Content_Types].xml" ||
        (
          before.audit.findings.length() > 0 &&
          p.name == relationship_part(before.audit.main_part)
        )
      )
    if removed {
      guard out is None else { raise Refused("VBA part retained") }
      dispositions.push({
        part: p.name,
        classification: "RemovedByPolicy",
        before_hash: p.sha256,
        after_hash: None,
      })
    } else {
      guard out is Some(q) else { raise Refused("preserved part missing") }
      guard metadata || q.sha256 == p.sha256 else {
        raise Refused("preserved payload changed")
      }
      dispositions.push({
        part: p.name,
        classification: if metadata {
          "RegeneratedMetadata"
        } else {
          "BytePreserved"
        },
        before_hash: p.sha256,
        after_hash: Some(q.sha256),
      })
    }
  }
  dispositions
}

///|
pub fn verify(
  output : Bytes,
  original : Bytes,
  limits? : Limits = Limits::default(),
) -> Receipt raise {
  guard output.length() <= limits.output_bytes else {
    raise Incomplete("output byte limit")
  }
  let before = load_package(original, limits)
  let expected_bytes = execute(before, limits)
  let expected = load_package(expected_bytes, limits)
  let after = load_package(output, limits)
  let dispositions = verify_parts(before, after, expected)
  {
    schema: "partsieve.receipt.spike-v1",
    tool: "partsieve/0.1.0-spike",
    rules: "spike-rules-v1",
    policy: "passive-office-v1",
    profile: before.audit.profile,
    input_hash: before.audit.input_hash,
    output_hash: digest(output[:]),
    input_size: original.length(),
    output_size: output.length(),
    input_format: before.audit.format,
    output_format: after.audit.format,
    limits,
    audit: before.audit,
    plan: make_plan(before),
    dispositions,
    structure: "Pass",
    preservation: "Pass",
    independent_validation: "NotChecked",
    limitations: before.audit.limitations,
  }
}

///|
pub fn rebuild(
  input : Bytes,
  limits? : Limits = Limits::default(),
) -> Rebuilt raise {
  let before = load_package(input, limits)
  let bytes = execute(before, limits)
  // Reparse serialized bytes; do not trust executor's graph.
  let receipt = verify(bytes, input, limits~)
  { bytes, receipt, }
}