///|
/// Severity used by the cross-layer model quality audit.
pub(all) enum QualityLevel {
  Pass
  Notice
  Warning
  Error
} derive(Debug, Eq)

///|
/// A concrete, actionable quality finding.
pub(all) struct QualityFinding {
  code : String
  level : QualityLevel
  message : String
  entity_id : Int?
  measured : Double
  threshold : Double
  repair_hint : String
} derive(Debug)

///|
/// Thresholds for the model quality audit.
pub(all) struct QualityPolicy {
  maximum_orphans : Int
  minimum_named_product_ratio : Double
  minimum_guid_ratio : Double
  maximum_duplicate_ids : Int
  maximum_unknown_entity_ratio : Double
  require_units : Bool
  require_spatial_structure : Bool
  require_geometry_for_products : Bool
} derive(Debug)

///|
/// A scorecard combining structural, semantic, unit and geometry signals.
pub(all) struct ModelQualityReport {
  findings : Array[QualityFinding]
  score : Int
  entity_count : Int
  product_count : Int
  orphan_count : Int
  duplicate_id_count : Int
  named_product_ratio : Double
  guid_ratio : Double
  unknown_entity_ratio : Double
} derive(Debug)

///|
pub fn default_quality_policy() -> QualityPolicy {
  {
    maximum_orphans: 0,
    minimum_named_product_ratio: 0.50,
    minimum_guid_ratio: 0.80,
    maximum_duplicate_ids: 0,
    maximum_unknown_entity_ratio: 0.45,
    require_units: false,
    require_spatial_structure: false,
    require_geometry_for_products: false,
  }
}

///|
pub fn QualityPolicy::allow_orphans(
  self : QualityPolicy,
  maximum_orphans : Int,
) -> QualityPolicy {
  {
    ..self,
    maximum_orphans: if maximum_orphans < 0 {
      0
    } else {
      maximum_orphans
    },
  }
}

///|
pub fn QualityPolicy::with_name_ratio(
  self : QualityPolicy,
  minimum_named_product_ratio : Double,
) -> QualityPolicy {
  {
    ..self,
    minimum_named_product_ratio: if minimum_named_product_ratio < 0.0 {
      0.0
    } else if minimum_named_product_ratio > 1.0 {
      1.0
    } else {
      minimum_named_product_ratio
    },
  }
}

///|
pub fn QualityPolicy::with_guid_ratio(
  self : QualityPolicy,
  minimum_guid_ratio : Double,
) -> QualityPolicy {
  {
    ..self,
    minimum_guid_ratio: if minimum_guid_ratio < 0.0 {
      0.0
    } else if minimum_guid_ratio > 1.0 {
      1.0
    } else {
      minimum_guid_ratio
    },
  }
}

///|
pub fn QualityPolicy::with_unknown_ratio(
  self : QualityPolicy,
  maximum_unknown_entity_ratio : Double,
) -> QualityPolicy {
  {
    ..self,
    maximum_unknown_entity_ratio: if maximum_unknown_entity_ratio < 0.0 {
      0.0
    } else if maximum_unknown_entity_ratio > 1.0 {
      1.0
    } else {
      maximum_unknown_entity_ratio
    },
  }
}

///|
pub fn QualityPolicy::requiring_units(
  self : QualityPolicy,
  required : Bool,
) -> QualityPolicy {
  { ..self, require_units: required }
}

///|
pub fn QualityPolicy::requiring_spatial_structure(
  self : QualityPolicy,
  required : Bool,
) -> QualityPolicy {
  { ..self, require_spatial_structure: required }
}

///|
pub fn QualityPolicy::requiring_geometry(
  self : QualityPolicy,
  required : Bool,
) -> QualityPolicy {
  { ..self, require_geometry_for_products: required }
}

///|
fn quality_level_name(level : QualityLevel) -> String {
  match level {
    Pass => "pass"
    Notice => "notice"
    Warning => "warning"
    Error => "error"
  }
}

///|
fn quality_schema_name(schema : SchemaVersion) -> String {
  match schema {
    IFC2X3 => "IFC2X3"
    IFC4 => "IFC4"
    IFC4X3 => "IFC4X3"
    UnknownSchema(name) => name
  }
}

///|
fn quality_score_delta(level : QualityLevel) -> Int {
  match level {
    Pass => 0
    Notice => 2
    Warning => 8
    Error => 20
  }
}

///|
fn quality_finding(
  code : String,
  level : QualityLevel,
  message : String,
  entity_id : Int?,
  measured : Double,
  threshold : Double,
  repair_hint : String,
) -> QualityFinding {
  { code, level, message, entity_id, measured, threshold, repair_hint }
}

///|
fn quality_add(
  findings : Array[QualityFinding],
  finding : QualityFinding,
) -> Unit {
  findings.push(finding)
}

///|
fn quality_duplicate_id_count(model : Model) -> Int {
  let counts : Map[Int, Int] = Map([])
  for entity in model.entities {
    counts.update_or_default(entity.id, 1, count => count + 1)
  }
  let mut duplicates = 0
  for _, count in counts {
    if count > 1 {
      duplicates += count - 1
    }
  }
  duplicates
}

///|
fn quality_named_ratio(products : Array[Product]) -> Double {
  if products.length() == 0 {
    1.0
  } else {
    products
    .fold(init=0, (count, product) => {
      match product.name {
        Some(name) if name.trim().length() > 0 => count + 1
        _ => count
      }
    })
    .to_double() /
    products.length().to_double()
  }
}

///|
fn quality_guid_ratio(products : Array[Product]) -> Double {
  if products.length() == 0 {
    1.0
  } else {
    products
    .fold(init=0, (count, product) => {
      match product.global_id {
        Some(guid) if validate_ifc_guid(guid).valid => count + 1
        _ => count
      }
    })
    .to_double() /
    products.length().to_double()
  }
}

///|
fn quality_unknown_ratio(profile : ModelProfile) -> Double {
  if profile.entity_count == 0 {
    0.0
  } else {
    profile.unknown_entity_count.to_double() / profile.entity_count.to_double()
  }
}

///|
fn quality_has_spatial(index : ModelIndex) -> Bool {
  index.ids_named("IFCPROJECT").length() > 0 &&
  (
    index.ids_named("IFCSITE").length() > 0 ||
    index.ids_named("IFCBUILDING").length() > 0 ||
    index.ids_named("IFCBUILDINGSTOREY").length() > 0
  )
}

///|
fn quality_has_geometry(index : ModelIndex, product : Product) -> Bool {
  geometries_for_product(index, product).any(geometry => {
    geometry.points.length() > 0 ||
    geometry.triangle_count > 0 ||
    geometry.depth is Some(_) ||
    geometry.profile_width is Some(_)
  })
}

///|
fn quality_add_ratio_finding(
  findings : Array[QualityFinding],
  code : String,
  label : String,
  ratio : Double,
  minimum : Double,
  hint : String,
) -> Unit {
  if ratio < minimum {
    quality_add(
      findings,
      quality_finding(
        code,
        Warning,
        label +
        " ratio " +
        ratio.to_string() +
        " is below " +
        minimum.to_string(),
        None,
        ratio,
        minimum,
        hint,
      ),
    )
  } else {
    quality_add(
      findings,
      quality_finding(
        code,
        Pass,
        label + " ratio meets the configured threshold",
        None,
        ratio,
        minimum,
        "no action required",
      ),
    )
  }
}

///|
fn quality_check_entities(
  model : Model,
  policy : QualityPolicy,
  findings : Array[QualityFinding],
) -> Int {
  let duplicate_count = quality_duplicate_id_count(model)
  if duplicate_count > policy.maximum_duplicate_ids {
    quality_add(
      findings,
      quality_finding(
        "STRUCT-DUPLICATE-ID",
        Error,
        "duplicate STEP entity ids detected: " + duplicate_count.to_string(),
        None,
        duplicate_count.to_double(),
        policy.maximum_duplicate_ids.to_double(),
        "deduplicate ids before indexing or exporting the model",
      ),
    )
  } else {
    quality_add(
      findings,
      quality_finding(
        "STRUCT-UNIQUE-ID",
        Pass,
        "all STEP entity ids are unique",
        None,
        duplicate_count.to_double(),
        policy.maximum_duplicate_ids.to_double(),
        "no action required",
      ),
    )
  }
  let empty_names = model.entities.fold(init=0, (count, entity) => {
    if entity.name.trim().length() == 0 {
      count + 1
    } else {
      count
    }
  })
  if empty_names > 0 {
    quality_add(
      findings,
      quality_finding(
        "STRUCT-EMPTY-TYPE",
        Error,
        "entities with empty type names: " + empty_names.to_string(),
        None,
        empty_names.to_double(),
        0.0,
        "preserve the original entity type when constructing custom entities",
      ),
    )
  } else {
    quality_add(
      findings,
      quality_finding(
        "STRUCT-TYPE-NAMES",
        Pass,
        "all entities have a type name",
        None,
        0.0,
        0.0,
        "no action required",
      ),
    )
  }
  duplicate_count
}

///|
fn quality_check_references(
  model : Model,
  policy : QualityPolicy,
  findings : Array[QualityFinding],
) -> Int {
  let orphan_ids = model_orphan_reference_ids(model)
  let orphan_count = orphan_ids.length()
  if orphan_count > policy.maximum_orphans {
    quality_add(
      findings,
      quality_finding(
        "REF-ORPHAN",
        Warning,
        "references target missing entities: " + orphan_count.to_string(),
        None,
        orphan_count.to_double(),
        policy.maximum_orphans.to_double(),
        "check deleted entities and preserve referenced definitions in the export",
      ),
    )
  } else {
    quality_add(
      findings,
      quality_finding(
        "REF-CLOSED",
        Pass,
        "reference graph is closed",
        None,
        orphan_count.to_double(),
        policy.maximum_orphans.to_double(),
        "no action required",
      ),
    )
  }
  orphan_count
}

///|
fn quality_check_schema(
  model : Model,
  findings : Array[QualityFinding],
) -> Unit {
  let schema = schema_version(model)
  match schema {
    IFC2X3 | IFC4 | IFC4X3 =>
      quality_add(
        findings,
        quality_finding(
          "SCHEMA-SUPPORTED",
          Pass,
          "recognized IFC schema " + quality_schema_name(schema),
          None,
          1.0,
          1.0,
          "no action required",
        ),
      )
    UnknownSchema(name) =>
      quality_add(
        findings,
        quality_finding(
          "SCHEMA-UNKNOWN",
          Notice,
          "schema identifier is not in the supported registry: " + name,
          None,
          0.0,
          1.0,
          "keep the header schema and select an explicit compatibility profile",
        ),
      )
  }
}

///|
fn quality_check_spatial(
  index : ModelIndex,
  policy : QualityPolicy,
  findings : Array[QualityFinding],
) -> Unit {
  let present = quality_has_spatial(index)
  if policy.require_spatial_structure && !present {
    quality_add(
      findings,
      quality_finding(
        "SPATIAL-MISSING",
        Error,
        "required project/site/building spatial structure is missing",
        None,
        0.0,
        1.0,
        "provide an IfcProject rooted spatial hierarchy",
      ),
    )
  } else if present {
    quality_add(
      findings,
      quality_finding(
        "SPATIAL-PRESENT",
        Pass,
        "project spatial structure is present",
        None,
        1.0,
        if policy.require_spatial_structure {
          1.0
        } else {
          0.0
        },
        "no action required",
      ),
    )
  } else {
    quality_add(
      findings,
      quality_finding(
        "SPATIAL-OPTIONAL",
        Notice,
        "spatial hierarchy is absent but not required by policy",
        None,
        0.0,
        0.0,
        "add spatial structure when the model is intended for coordination",
      ),
    )
  }
}

///|
fn quality_check_units(
  model : Model,
  policy : QualityPolicy,
  findings : Array[QualityFinding],
) -> Unit {
  let report = unit_audit(model)
  if policy.require_units && report.row_count() == 0 {
    quality_add(
      findings,
      quality_finding(
        "UNIT-MISSING",
        Error,
        "no IFC unit declarations found",
        None,
        0.0,
        1.0,
        "declare IfcUnitAssignment and preserve measurement semantics",
      ),
    )
  } else if report.is_consistent() {
    quality_add(
      findings,
      quality_finding(
        "UNIT-CONSISTENT",
        Pass,
        report.headline(),
        None,
        report.row_count().to_double(),
        0.0,
        "no action required",
      ),
    )
  } else {
    quality_add(
      findings,
      quality_finding(
        "UNIT-INCONSISTENT",
        Warning,
        report.headline(),
        None,
        report.unknown_count.to_double() +
        report.duplicate_kind_count.to_double(),
        0.0,
        "normalize unit aliases and retain one declaration per dimensional kind",
      ),
    )
  }
}

///|
fn quality_check_products(
  model : Model,
  policy : QualityPolicy,
  findings : Array[QualityFinding],
) -> (Double, Double) {
  let products = model.products()
  let named_ratio = quality_named_ratio(products)
  let guid_ratio = quality_guid_ratio(products)
  quality_add_ratio_finding(
    findings,
    "PRODUCT-NAMES",
    "named product",
    named_ratio,
    policy.minimum_named_product_ratio,
    "populate IfcRoot.Name for coordination and schedule exports",
  )
  quality_add_ratio_finding(
    findings,
    "PRODUCT-GUIDS",
    "valid global id",
    guid_ratio,
    policy.minimum_guid_ratio,
    "preserve valid 22-character IFC compressed GUIDs",
  )
  (named_ratio, guid_ratio)
}

///|
fn quality_check_geometry(
  model : Model,
  policy : QualityPolicy,
  findings : Array[QualityFinding],
) -> Unit raise ParseError {
  let index = model.index()
  let products = model.products()
  let represented = products.fold(init=0, (count, product) => {
    if quality_has_geometry(index, product) {
      count + 1
    } else {
      count
    }
  })
  let ratio = if products.length() == 0 {
    1.0
  } else {
    represented.to_double() / products.length().to_double()
  }
  if policy.require_geometry_for_products && represented < products.length() {
    quality_add(
      findings,
      quality_finding(
        "GEOMETRY-INCOMPLETE",
        Warning,
        "products with usable geometry: " +
        represented.to_string() +
        "/" +
        products.length().to_string(),
        None,
        ratio,
        1.0,
        "check representation links and retain supported solid or mesh entities",
      ),
    )
  } else {
    quality_add(
      findings,
      quality_finding(
        "GEOMETRY-COVERAGE",
        if ratio == 1.0 {
          Pass
        } else {
          Notice
        },
        "geometry coverage ratio: " + ratio.to_string(),
        None,
        ratio,
        if policy.require_geometry_for_products {
          1.0
        } else {
          0.0
        },
        "inspect geometry inventory for products without representation",
      ),
    )
  }
  ignore(geometry_inventory(model))
}

///|
/// Run the complete quality audit with deterministic finding order.
pub fn model_quality_audit(
  model : Model,
  policy? : QualityPolicy = default_quality_policy(),
) -> ModelQualityReport raise ParseError {
  let findings : Array[QualityFinding] = []
  quality_check_schema(model, findings)
  let duplicate_count = quality_check_entities(model, policy, findings)
  let orphan_count = quality_check_references(model, policy, findings)
  let index = model.index()
  quality_check_spatial(index, policy, findings)
  quality_check_units(model, policy, findings)
  let (named_ratio, guid_ratio) = quality_check_products(
    model, policy, findings,
  )
  quality_check_geometry(model, policy, findings)
  let profile = model_profile(model)
  let unknown_ratio = quality_unknown_ratio(profile)
  quality_add_ratio_finding(
    findings,
    "SCHEMA-KNOWN",
    "known entity",
    1.0 - unknown_ratio,
    1.0 - policy.maximum_unknown_entity_ratio,
    "register or preserve extension entities before schema-specific processing",
  )
  let penalty = findings.fold(init=0, (sum, finding) => {
    sum + quality_score_delta(finding.level)
  })
  let score = if penalty >= 100 { 0 } else { 100 - penalty }
  {
    findings,
    score,
    entity_count: model.entities.length(),
    product_count: model.products().length(),
    orphan_count,
    duplicate_id_count: duplicate_count,
    named_product_ratio: named_ratio,
    guid_ratio,
    unknown_entity_ratio: unknown_ratio,
  }
}

///|
pub fn ModelQualityReport::is_healthy(self : ModelQualityReport) -> Bool {
  self.findings.all(finding => {
    finding.level != Error && finding.level != Warning
  })
}

///|
pub fn ModelQualityReport::has_errors(self : ModelQualityReport) -> Bool {
  self.findings.any(finding => finding.level == Error)
}

///|
pub fn ModelQualityReport::finding_count(self : ModelQualityReport) -> Int {
  self.findings.length()
}

///|
pub fn ModelQualityReport::count_level(
  self : ModelQualityReport,
  level : QualityLevel,
) -> Int {
  self.findings.fold(init=0, (count, finding) => {
    if finding.level == level {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn ModelQualityReport::findings_at(
  self : ModelQualityReport,
  level : QualityLevel,
) -> Array[QualityFinding] {
  self.findings.filter(finding => finding.level == level)
}

///|
pub fn ModelQualityReport::finding_code(
  self : ModelQualityReport,
  code : String,
) -> QualityFinding? {
  for finding in self.findings {
    if finding.code == code {
      return Some(finding)
    }
  }
  None
}

///|
pub fn QualityFinding::headline(self : QualityFinding) -> String {
  "[" + quality_level_name(self.level) + "] " + self.code + ": " + self.message
}

///|
pub fn QualityFinding::to_json(self : QualityFinding) -> String {
  let id = match self.entity_id {
    Some(value) => value.to_string()
    None => "null"
  }
  "{\"code\":\"" +
  self.code +
  "\",\"level\":\"" +
  quality_level_name(self.level) +
  "\",\"message\":\"" +
  self.message +
  "\",\"entity_id\":" +
  id +
  ",\"measured\":" +
  self.measured.to_string() +
  ",\"threshold\":" +
  self.threshold.to_string() +
  ",\"repair_hint\":\"" +
  self.repair_hint +
  "\"}"
}

///|
pub fn ModelQualityReport::headline(self : ModelQualityReport) -> String {
  "score=" +
  self.score.to_string() +
  " entities=" +
  self.entity_count.to_string() +
  " products=" +
  self.product_count.to_string() +
  " findings=" +
  self.finding_count().to_string() +
  " errors=" +
  self.count_level(Error).to_string() +
  " warnings=" +
  self.count_level(Warning).to_string()
}

///|
pub fn ModelQualityReport::to_json(self : ModelQualityReport) -> String {
  "{\"score\":" +
  self.score.to_string() +
  ",\"entity_count\":" +
  self.entity_count.to_string() +
  ",\"product_count\":" +
  self.product_count.to_string() +
  ",\"orphan_count\":" +
  self.orphan_count.to_string() +
  ",\"duplicate_id_count\":" +
  self.duplicate_id_count.to_string() +
  ",\"named_product_ratio\":" +
  self.named_product_ratio.to_string() +
  ",\"guid_ratio\":" +
  self.guid_ratio.to_string() +
  ",\"unknown_entity_ratio\":" +
  self.unknown_entity_ratio.to_string() +
  ",\"findings\":[" +
  self.findings.map(finding => finding.to_json()).join(",") +
  "]}"
}

///|
pub fn ModelQualityReport::to_markdown(self : ModelQualityReport) -> String {
  let builder = StringBuilder()
  builder <+ "# Model quality audit\n\n"
  builder <+ "- \{self.headline()}\n"
  builder <+ "- Named product ratio: \{self.named_product_ratio}\n"
  builder <+ "- Valid GUID ratio: \{self.guid_ratio}\n"
  builder <+ "- Unknown entity ratio: \{self.unknown_entity_ratio}\n\n"
  builder <+ "| Level | Code | Finding | Repair hint |\n"
  builder <+ "| --- | --- | --- | --- |\n"
  for finding in self.findings {
    builder <+
      "| \{quality_level_name(finding.level)} | \{finding.code} | \{finding.message} | \{finding.repair_hint} |\n"
  }
  builder.to_string()
}

///|
pub fn model_quality_summary(model : Model) -> Array[String] raise ParseError {
  let report = model_quality_audit(model)
  [
    report.headline(),
    "healthy=" + report.is_healthy().to_string(),
    "named_ratio=" + report.named_product_ratio.to_string(),
    "guid_ratio=" + report.guid_ratio.to_string(),
  ]
}

///|
pub fn model_quality_errors(
  model : Model,
) -> Array[QualityFinding] raise ParseError {
  model_quality_audit(model).findings_at(Error)
}

///|
pub fn model_quality_warnings(
  model : Model,
) -> Array[QualityFinding] raise ParseError {
  model_quality_audit(model).findings_at(Warning)
}