///|
let spreadsheet_ns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"

///|
let drawing_ns = "http://schemas.openxmlformats.org/drawingml/2006/main"

///|
fn validate_profile(pkg : Package) -> Unit raise {
  let report = pkg.audit
  let allowed_rels = [
    office_rel + "officeDocument",
    office_rel + "worksheet",
    office_rel + "styles",
    office_rel + "theme",
    office_rel + "sharedStrings",
    office_rel + "extended-properties",
    "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
    vba_rel,
  ]
  for r in report.relationships {
    guard !r.external && allowed_rels.contains(r.kind) else {
      raise Refused("external or unsupported relationship capability")
    }
    let target = report.parts.filter(fn(p) { p.name == r.resolved })[0]
    let expected = if r.kind == office_rel + "officeDocument" {
      target.content_type == xlsx_ct || target.content_type == xlsm_ct
    } else if r.kind == vba_rel {
      target.content_type == vba_ct
    } else if r.kind ==
      "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" {
      target.content_type ==
      "application/vnd.openxmlformats-package.core-properties+xml"
    } else {
      let suffix = r.kind[office_rel.length():].to_owned()
      let wanted = if suffix == "extended-properties" {
        "application/vnd.openxmlformats-officedocument.extended-properties+xml"
      } else {
        "application/vnd.openxmlformats-officedocument." +
        (if suffix == "theme" { "theme" } else { "spreadsheetml." + suffix }) +
        "+xml"
      }
      target.content_type == wanted
    }
    guard expected else {
      raise Refused("relationship/content-type evidence conflict")
    }
    guard (if r.kind == vba_rel ||
      r.kind == office_rel + "worksheet" ||
      r.kind == office_rel + "styles" ||
      r.kind == office_rel + "theme" ||
      r.kind == office_rel + "sharedStrings" {
      r.source == report.main_part
    } else {
      r.source == "/"
    }) else {
      raise Refused("unsupported relationship source")
    }
  }
  guard report.findings.length() <= 1 else {
    raise Refused("multiple VBA parts unsupported")
  }
  for f in report.findings {
    guard report.format == "XLSM" else {
      raise Refused("VBA in non-macro main type")
    }
    let incoming = report.relationships.filter(fn(r) { r.resolved == f.part })
    guard incoming.length() == 1 &&
      incoming[0].kind == vba_rel &&
      incoming[0].source == report.main_part else {
      raise Refused("orphan/shared VBA or evidence conflict")
    }
    guard pkg.archive.get(relationship_part(f.part)[:]) is None else {
      raise Refused("VBA companion relationship parts unsupported")
    }
    guard get_part(pkg.archive, f.part).has_prefix(
      b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1",
    ) else {
      raise Refused("VBA part is not a CFB container")
    }
  }
  let static_types = [
    xlsm_ct, xlsx_ct, vba_ct, "application/vnd.openxmlformats-package.relationships+xml",
    "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
    "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
    "application/vnd.openxmlformats-officedocument.theme+xml", "application/vnd.openxmlformats-package.core-properties+xml",
    "application/vnd.openxmlformats-officedocument.extended-properties+xml",
  ]
  for p in report.parts {
    if p.name == "[Content_Types].xml" {
      continue
    }
    guard static_types.contains(p.content_type) else {
      raise Refused("unknown/unsupported part content type")
    }
    if p.content_type == vba_ct {
      continue
    }
    if p.name.has_suffix(".rels") {
      guard p.content_type ==
        "application/vnd.openxmlformats-package.relationships+xml" else {
        raise Refused("relationship part content type mismatch")
      }
      continue
    }
    guard pkg.xml_nodes.get(p.name) is Some(nodes) else {
      raise Refused("XML content type on unsupported payload name")
    }
    let root = nodes[0].element.name.local_name
    let expected_root = if p.name == report.main_part {
      "workbook"
    } else if p.content_type.contains(".worksheet+") {
      "worksheet"
    } else if p.content_type.contains(".styles+") {
      "styleSheet"
    } else if p.content_type.contains(".sharedStrings+") {
      "sst"
    } else if p.content_type.contains(".theme+") {
      "theme"
    } else if p.content_type.contains("core-properties") {
      "coreProperties"
    } else {
      "Properties"
    }
    guard root == expected_root else {
      raise Refused("part root/content type mismatch")
    }
    let parents : Array[String] = []
    let sheet_refs : Array[String] = []
    let sheet_names : Array[String] = []
    for node in nodes {
      let ns = node.element.name.namespace_uri.unwrap_or("")
      let local_name = node.element.name.local_name
      while parents.length() > node.depth {
        ignore(parents.pop())
      }
      let parent = if parents.length() > 0 {
        parents[parents.length() - 1]
      } else {
        ""
      }
      if expected_root == "workbook" {
        let shape_ok = match local_name {
          "workbook" => node.depth == 0
          "fileVersion" | "workbookPr" | "bookViews" | "sheets" | "calcPr" =>
            parent == "workbook"
          "workbookView" => parent == "bookViews"
          "sheet" => parent == "sheets"
          _ => false
        }
        guard shape_ok else {
          raise Refused("unsupported workbook element placement")
        }
        if local_name == "sheet" {
          let refs = node.element.attributes.filter(fn(a) {
            a.name.namespace_uri ==
            Some(office_rel[0:office_rel.length() - 1].to_owned()) &&
            a.name.local_name == "id"
          })
          guard refs.length() == 1 && !sheet_refs.contains(refs[0].value) else {
            raise Refused("missing or reused worksheet XML relationship ID")
          }
          sheet_refs.push(refs[0].value)
          let sheet_name = attr(node, "name")
          guard sheet_name.length() > 0 &&
            sheet_name.length() <= 31 &&
            !sheet_names.contains(sheet_name.to_lower()) else {
            raise Refused("invalid or duplicate sheet name")
          }
          sheet_names.push(sheet_name.to_lower())
          ignore(attr(node, "sheetId"))
        }
      } else if expected_root == "worksheet" {
        let shape_ok = match local_name {
          "worksheet" => node.depth == 0
          "sheetPr"
          | "dimension"
          | "sheetViews"
          | "sheetFormatPr"
          | "cols"
          | "sheetData"
          | "pageMargins"
          | "pageSetup"
          | "printOptions"
          | "mergeCells" => parent == "worksheet"
          "pageSetUpPr" | "outlinePr" => parent == "sheetPr"
          "sheetView" => parent == "sheetViews"
          "selection" => parent == "sheetView"
          "col" => parent == "cols"
          "row" => parent == "sheetData"
          "c" => parent == "row"
          "v" | "is" => parent == "c"
          "t" => parent == "is" || parent == "r"
          "r" => parent == "is"
          "rPr" => parent == "r"
          "mergeCell" => parent == "mergeCells"
          _ => false
        }
        guard shape_ok else {
          raise Refused("unsupported worksheet element placement")
        }
      }
      parents.push(local_name)
      let permitted = if expected_root == "workbook" {
        ns == spreadsheet_ns &&
        [
          "workbook", "fileVersion", "workbookPr", "bookViews", "workbookView", "sheets",
          "sheet", "calcPr",
        ].contains(local_name)
      } else if expected_root == "worksheet" {
        ns == spreadsheet_ns &&
        [
          "worksheet", "sheetPr", "pageSetUpPr", "outlinePr", "dimension", "sheetViews",
          "sheetView", "selection", "sheetFormatPr", "cols", "col", "sheetData",
          "row", "c", "v", "is", "t", "r", "rPr", "pageMargins", "pageSetup", "printOptions",
          "mergeCells", "mergeCell",
        ].contains(local_name)
      } else if expected_root == "sst" {
        ns == spreadsheet_ns &&
        [
          "sst", "si", "t", "r", "rPr", "b", "i", "u", "sz", "color", "rFont", "family",
          "scheme", "vertAlign",
        ].contains(local_name)
      } else if expected_root == "styleSheet" {
        ns == spreadsheet_ns &&
        [
          "styleSheet", "numFmts", "numFmt", "fonts", "font", "b", "i", "u", "sz",
          "color", "name", "family", "scheme", "vertAlign", "fills", "fill", "patternFill",
          "fgColor", "bgColor", "borders", "border", "left", "right", "top", "bottom",
          "diagonal", "cellStyleXfs", "cellXfs", "xf", "alignment", "protection",
          "cellStyles", "cellStyle", "dxfs", "tableStyles",
        ].contains(local_name)
      } else if expected_root == "theme" {
        ns == drawing_ns &&
        [
          "theme", "themeElements", "clrScheme", "dk1", "lt1", "dk2", "lt2", "accent1",
          "accent2", "accent3", "accent4", "accent5", "accent6", "hlink", "folHlink",
          "sysClr", "srgbClr", "fontScheme", "majorFont", "minorFont", "latin", "ea",
          "cs", "font", "fmtScheme", "fillStyleLst", "solidFill", "schemeClr", "gradFill",
          "gsLst", "gs", "tint", "satMod", "shade", "lin", "path", "fillToRect",
          "lnStyleLst", "ln", "prstDash", "miter", "effectStyleLst", "effectStyle",
          "effectLst", "outerShdw", "alpha", "bgFillStyleLst", "objectDefaults",
          "extraClrSchemeLst", "scene3d", "camera", "lightRig", "rot", "sp3d", "bevelT",
        ].contains(local_name)
      } else if expected_root == "coreProperties" {
        [
          "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
          "http://purl.org/dc/elements/1.1/", "http://purl.org/dc/terms/",
        ].contains(ns) &&
        [
          "coreProperties", "title", "subject", "creator", "keywords", "description",
          "lastModifiedBy", "revision", "created", "modified", "category", "contentStatus",
          "lastPrinted", "version", "identifier", "language",
        ].contains(local_name)
      } else {
        [
          "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties",
          "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes",
        ].contains(ns) &&
        [
          "Properties", "Application", "AppVersion", "DocSecurity", "ScaleCrop",
          "HeadingPairs", "TitlesOfParts", "Company", "LinksUpToDate", "SharedDoc",
          "HyperlinksChanged", "vector", "variant", "lpstr", "i4",
        ].contains(local_name)
      }
      guard permitted else {
        raise Refused("unsupported XML namespace or feature: " + local_name)
      }
      for a in node.element.attributes {
        let ans = a.name.namespace_uri.unwrap_or("")
        guard ans == "" ||
          ans == "http://www.w3.org/XML/1998/namespace" ||
          ans == office_rel[0:office_rel.length() - 1].to_owned() ||
          (
            expected_root == "coreProperties" &&
            ans == "http://www.w3.org/2001/XMLSchema-instance"
          ) else {
          raise Refused("unsupported namespaced XML attribute")
        }
        if ans == office_rel[0:office_rel.length() - 1].to_owned() {
          guard expected_root == "workbook" &&
            local_name == "sheet" &&
            a.name.local_name == "id" else {
            raise Refused("unsupported source XML relationship reference")
          }
          let match_rel = report.relationships.filter(fn(r) {
            r.source == p.name && r.id == a.value
          })
          guard match_rel.length() == 1 &&
            match_rel[0].kind == office_rel + "worksheet" else {
            raise Refused("dangling or wrongly-bound source XML reference")
          }
        }
      }
    }
    if expected_root == "workbook" {
      guard sheet_refs.length() > 0 else {
        raise Refused("workbook has no supported sheet")
      }
      for relation in report.relationships {
        if relation.source == p.name &&
          relation.kind == office_rel + "worksheet" {
          guard sheet_refs.contains(relation.id) else {
            raise Refused("unreferenced worksheet relationship")
          }
        }
      }
    }
  }
}