///|
fn make_plan(pkg : Package) -> RewritePlan {
  let report = pkg.audit
  let ops : Array[Operation] = []
  for f in report.findings {
    let rel = graph_incoming(pkg.graph, 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: if is_word_format(report.format) {
        "VBA is an implicit main-document relationship; no supported source XML references it."
      } else {
        "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.",
    })
  }
  for part in controlled_vba_parts(pkg) {
    if !report.findings.any(fn(f) { f.part == part }) {
      ops.push({
        kind: "RemovePart",
        part,
        relationship_id: "",
        rule: "vba-v1",
        reason: "Remove exclusively owned Word VBA supporting metadata and its relationship part.",
      })
    }
  }
  if is_macro_format(report.format) {
    ops.push({
      kind: "ChangeMainContentType",
      part: report.main_part,
      relationship_id: "",
      rule: "format-v1",
      reason: if is_word_format(report.format) {
        "Convert DOCM to DOCX, including macro-enabled formats without actual VBA."
      } else {
        "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: if is_word_format(report.format) {
      "partsieve.plan.word-dev-v1"
    } else {
      "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 && ct != word_vba_data_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 || controlled_vba_parts(pkg).contains(name) {
      continue
    }
    writer.empty_element("Override", [
      ("PartName", "/" + name),
      (
        "ContentType",
        if name == pkg.audit.main_part {
          plain_content_type(pkg)
        } 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", plain_content_type(pkg)),
    ])
  }
  writer.end_element("Types")
  @utf8.encode(writer.to_string()[:])
}

///|
fn execute(pkg : Package, limits : Limits) -> Bytes raise {
  execute_prepared(pkg, make_prepared_plan(pkg, limits), limits)
}

///|
fn verify_parts(
  before : Package,
  after : Package,
  expected : Package,
) -> Array[Disposition] raise {
  let dispositions : Array[Disposition] = []
  guard after.audit.format ==
    (if is_word_format(before.audit.format) { "DOCX" } else { "XLSX" }) &&
    after.audit.findings.length() == 0 else {
    raise Invalid("output has forbidden VBA or macro-enabled type")
  }
  guard after.graph.graph == expected.graph.graph else {
    raise Invalid("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 = controlled_vba_parts(before).contains(p.name)
    let metadata = is_macro_format(before.audit.format) &&
      (
        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 Invalid("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 Invalid("preserved part missing") }
      guard metadata || q.sha256 == p.sha256 else {
        raise Invalid("preserved payload changed")
      }
      dispositions.push({
        part: p.name,
        classification: if metadata {
          "RegeneratedMetadata"
        } else {
          "BytePreserved"
        },
        before_hash: p.sha256,
        after_hash: Some(q.sha256),
      })
    }
  }
  dispositions
}

///|
priv struct VerifiedPackages {
  before : Package
  after : Package
  dispositions : Array[Disposition]
}

///|
fn verify_packages(
  output : Bytes,
  original : Bytes,
  limits : Limits,
) -> VerifiedPackages 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, output=true)
  let after = load_package(output, limits, output=true)
  let dispositions = verify_parts(before, after, expected)
  { before, after, dispositions, }
}

///|
fn legacy_receipt(
  checked : VerifiedPackages,
  output : Bytes,
  original : Bytes,
  limits : Limits,
) -> Receipt {
  let before = checked.before
  let after = checked.after
  {
    schema: if is_word_format(before.audit.format) {
      "partsieve.receipt.word-dev-v1"
    } else {
      "partsieve.receipt.spike-v1"
    },
    tool: if is_word_format(before.audit.format) {
      "partsieve/0.2.0-spike"
    } else {
      "partsieve/0.1.0-spike"
    },
    rules: if is_word_format(before.audit.format) {
      "word-vba-dev-v1"
    } else {
      "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: checked.dispositions,
    structure: "Pass",
    preservation: "Pass",
    independent_validation: "NotChecked",
    limitations: before.audit.limitations,
  }
}

///|
pub fn verify(
  output : Bytes,
  original : Bytes,
  limits? : Limits = Limits::default(),
) -> Receipt raise {
  legacy_receipt(
    verify_packages(output, original, limits),
    output,
    original,
    limits,
  )
}

///|
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, }
}