///|
priv struct Package {
  archive : @zip.Archive
  audit : Audit
  xml_nodes : Map[String, Array[Node]]
  defaults : Map[String, String]
  overrides : Map[String, String]
  graph : GraphFacts
  control : WorkControl
}

///|
fn get_part(archive : @zip.Archive, name : String) -> BytesView raise {
  match archive.get(name[:]) {
    Some(data) => data
    None => raise Refused("required part missing: " + name)
  }
}

///|
fn load_package(
  input : Bytes,
  limits : Limits,
  enforce_profile? : Bool = true,
  output? : Bool = false,
  control? : WorkControl = WorkControl::default(),
) -> Package raise {
  control.checkpoint("package-start", 0)
  limits.validate()
  let package_limit = if output {
    limits.output_bytes
  } else {
    limits.input_bytes
  }
  guard input.length() <= package_limit else {
    raise Incomplete(
      "ZIP parsing did not complete: zip: package exceeds the input size limit",
    )
  }
  control.checkpoint("package-bytes", input.length())
  let zl = @zip.ReadLimits::default()
    .with_package_limit(package_limit)
    .with_entry_limit(limits.entry_bytes)
    .with_total_limit(Int::min(limits.total_bytes, control.remaining()))
    .with_entries_limit(limits.entries)
    .with_preserved_source_limit(package_limit * 2)
  let archive = @zip.read(input[:], limits=zl, cancelled=fn() {
    control.poll("zip-read", 1024)
  }) catch {
    @zip.ZipError(_, message) => {
      control.checkpoint("zip-read-complete", 0)
      raise Incomplete("ZIP parsing did not complete: " + message)
    }
  }
  let names : Map[String, Bool] = Map([])
  let aliases : Map[String, Bool] = Map([])
  let ordered : Array[String] = []
  for entry in archive.entries() {
    control.checkpoint("zip-entry-crc", entry.data().length())
    let name = entry.name()
    check_name(name)
    guard !names.contains(name) && !aliases.contains(name.to_lower()) else {
      raise Refused("duplicate or case-equivalent part name")
    }
    names[name] = true
    aliases[name.to_lower()] = true
    ordered.push(name)
    guard @checksum.crc32(entry.data()) == entry.crc32() else {
      raise Invalid("ZIP payload CRC mismatch")
    }
  }
  ordered.sort()
  let defaults : Map[String, String] = Map([])
  let overrides : Map[String, String] = Map([])
  let ct_nodes = metadata_nodes(
    get_part(archive, "[Content_Types].xml"),
    limits,
    ct_ns,
    "Types",
    ["Default", "Override"],
    control~,
  )
  for i in 1.. t
        None =>
          match name.rev_split_once(".") {
            Some((_, extension)) =>
              match defaults.get(extension.to_owned()) {
                Some(t) => t
                None => raise Refused("part has no content type")
              }
            None => raise Refused("part has no content type")
          }
      }
    }
    let data = get_part(archive, name)
    parts.push({ name, content_type: ct, sha256: work_digest(data, control), })
    if name.has_suffix(".xml") || name.has_suffix(".rels") {
      let parsed = read_xml(data, limits, control~)
      guard parsed.length() > 0 else { raise Refused("XML root missing") }
      total_nodes += parsed.length()
      for node in parsed {
        total_attributes += node.element.attributes.length()
      }
      guard total_nodes <= 1000000 && total_attributes <= 1000000 else {
        raise Incomplete("package XML aggregate limit")
      }
      nodes[name] = parsed
    }
  }
  let relationships : Array[Relationship] = []
  for name in ordered {
    if !name.has_suffix(".rels") {
      continue
    }
    let source = relationship_source(name)
    guard source == "/" || names.contains(source) else {
      raise Invalid("relationship source missing")
    }
    let local_ids : Map[String, Bool] = Map([])
    let rels = metadata_nodes(
      get_part(archive, name),
      limits,
      rel_ns,
      "Relationships",
      ["Relationship"],
      control~,
    )
    for i in 1.. 0 && id.length() <= 1024 && !local_ids.contains(id) else {
        raise Invalid("duplicate or invalid local relationship ID")
      }
      local_ids[id] = true
      let kind = attr(node, "Type")
      let target = attr(node, "Target")
      let mode = attr_opt(node, "TargetMode").unwrap_or("Internal")
      guard mode == "Internal" || mode == "External" else {
        raise Refused("invalid target mode")
      }
      let external = mode == "External"
      let resolved = if external { "" } else { resolve_target(source, target) }
      guard external || names.contains(resolved) else {
        raise Invalid("internal target missing")
      }
      relationships.push({ source, id, kind, target, resolved, external, })
    }
  }
  let mains = relationships.filter(fn(r) {
    r.source == "/" && r.kind == office_rel + "officeDocument"
  })
  guard mains.length() == 1 && !mains[0].external else {
    raise Invalid("exactly one internal office main required")
  }
  let main = mains[0].resolved
  let main_ct = parts.filter(fn(p) { p.name == main })[0].content_type
  guard !enforce_profile ||
    main_ct == xlsm_ct ||
    main_ct == xlsx_ct ||
    main_ct == docm_ct ||
    main_ct == docx_ct else {
    raise Refused("unsupported main document type")
  }
  let graph = build_graph(parts, relationships, nodes, control~)
  let findings : Array[Finding] = []
  for p in parts {
    if p.content_type == vba_ct {
      let incoming = graph_incoming(graph, p.name)
      findings.push({
        rule: "vba-v1",
        capability: "VBA",
        part: p.name,
        evidence: ["content-type:" + vba_ct] +
        incoming.map(fn(r) {
          "relationship:" + r.source + "#" + r.id + ":" + r.kind
        }),
      })
    }
  }
  let report : Audit = {
    schema: if main_ct == docm_ct || main_ct == docx_ct {
      "partsieve.audit.word-dev-v1"
    } else {
      "partsieve.audit.spike-v1"
    },
    input_hash: work_digest(input[:], control),
    format: if main_ct == xlsm_ct {
      "XLSM"
    } else if main_ct == xlsx_ct {
      "XLSX"
    } else if main_ct == docm_ct {
      "DOCM"
    } else if main_ct == docx_ct {
      "DOCX"
    } else {
      "Unsupported"
    },
    main_part: main,
    coverage: "CompleteWithinProfile",
    profile: if relationships.any(fn(r) {
        hyperlink_declared(r, parts, nodes) ||
        ole_declared(r, graph.ole_objects)
      }) {
      if main_ct == docm_ct || main_ct == docx_ct {
        "business-word-v2"
      } else {
        "business-spreadsheet-v2"
      }
    } else if main_ct == docm_ct || main_ct == docx_ct {
      "business-word-v1"
    } else {
      "business-spreadsheet-v1"
    },
    checked_rules: [
      "bounded-zip-v1", "crc-v1", "opc-subset-v1", "namespace-xml-v1", "vba-v1",
      "profile-v1",
    ],
    limitations: if relationships.any(fn(r) {
        hyperlink_declared(r, parts, nodes) ||
        ole_declared(r, graph.ole_objects)
      }) {
      [
        "Extended business profile: source-bound HTTP(S) click hyperlinks retained without network access; up to 32 independent inline main-document embedded OLE objects with private PNG/JPEG/EMF previews can be removed entirely. Linked/shared/unknown objects, ActiveX, fields, formulas and unsupported schemes refuse.",
        "No VBA or OLE execution, recursive payload sanitization, visual fidelity or general safety claim. OLE preview content is intentionally removed with the object.",
      ]
    } else {
      [
        "Business profiles: text, serial dates, multiple sheets and common styles; Word tables, inline PNG/JPEG, headers/footers. Formulas, hyperlinks, floating/unknown drawings, controls, signatures and unknown extensions refused.",
        "No VBA execution or malicious-code analysis. No visual fidelity claim. Client/SDK checks recorded separately.",
      ]
    },
    parts,
    relationships,
    findings,
  }
  let pkg = {
    archive,
    audit: report,
    xml_nodes: nodes,
    defaults,
    overrides,
    graph,
    control,
  }
  if enforce_profile {
    require_supported_profile(pkg)
  }
  control.checkpoint("package-complete", 0)
  pkg
}

///|
fn validate_metadata_attributes(
  node : Node,
  permitted : Array[String],
) -> Unit raise {
  for a in node.element.attributes {
    guard a.name.namespace_uri is None && permitted.contains(a.name.local_name) else {
      raise Refused("unsupported metadata attribute")
    }
  }
}

///|
pub fn audit(
  input : Bytes,
  limits? : Limits = Limits::default(),
  control? : WorkControl = WorkControl::default(),
) -> Audit raise {
  load_package(input, limits, control~).audit
}