///|
pub(all) enum AuditRule {
  HeaderRule
  NameMapRule
  NetIntegrityRule
  RcValueRule
  TopologyRule
  ComplexityRule
} derive(Eq, Debug)

///|
pub(all) struct AuditConfig {
  strict : Bool
  capacitance_relative_tolerance : Double
  complexity_threshold : Int
} derive(Debug)

///|
pub(all) struct AuditReport {
  diagnostics : Array[@diagnostic.Diagnostic]
  error_count : Int
  warning_count : Int
  info_count : Int
  passed : Bool
} derive(Debug)

///|
pub fn AuditConfig::default() -> AuditConfig {
  {
    strict: false,
    capacitance_relative_tolerance: 0.05,
    complexity_threshold: 1000,
  }
}

///|
pub fn default_rules() -> Array[AuditRule] {
  [
    HeaderRule,
    NameMapRule,
    NetIntegrityRule,
    RcValueRule,
    TopologyRule,
    ComplexityRule,
  ]
}

///|
fn magnitude(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
fn invalid_number(value : Double) -> Bool {
  value.is_nan() || value.is_inf()
}

///|
fn add_issue(
  issues : Array[@diagnostic.Diagnostic],
  code : String,
  severity : @diagnostic.Severity,
  message : String,
  suggestion : String,
  span : @diagnostic.SourceSpan?,
  net? : String,
) -> Unit {
  issues.push({
    code,
    severity,
    message,
    suggestion: Some(suggestion),
    span,
    net,
  })
}

///|
fn valid_node(net : @model.Net, id : Int) -> Bool {
  id >= 0 && id < net.nodes.length()
}

///|
fn check_headers(
  network : @model.RCNetwork,
  issues : Array[@diagnostic.Diagnostic],
) -> Unit {
  if network.header.spef_version is None {
    add_issue(
      issues,
      "SPEF-A001",
      @diagnostic.Error,
      "missing *SPEF standard declaration",
      "add *SPEF \"IEEE 1481-1998\" near the beginning of the file",
      None,
    )
  }
  if network.header.design_name is None {
    add_issue(
      issues,
      "SPEF-A002",
      @diagnostic.Warning,
      "missing *DESIGN declaration",
      "add the design name to make reports traceable",
      None,
    )
  }
}

///|
fn check_name_map(
  network : @model.RCNetwork,
  issues : Array[@diagnostic.Diagnostic],
) -> Unit {
  if network.name_map.length() == 0 {
    add_issue(
      issues,
      "SPEF-A010",
      @diagnostic.Warning,
      "no *NAME_MAP section was found",
      "add a name map for large designs; literal-only SPEF remains valid",
      None,
    )
  }
  for left in 0.. Unit {
  for net in network.nets {
    if net.connections.length() == 0 {
      add_issue(
        issues,
        "SPEF-A100",
        @diagnostic.Error,
        "network \{net.name} has no connection declarations",
        "add *P or *I entries under *CONN",
        net.span,
        net=net.name,
      )
    }
    if net.nodes.length() == 0 {
      add_issue(
        issues,
        "SPEF-A101",
        @diagnostic.Error,
        "network \{net.name} contains no nodes",
        "add connection and parasitic entries or remove the empty net",
        net.span,
        net=net.name,
      )
    }
    for connection in net.connections {
      if !valid_node(net, connection.node_id) {
        add_issue(
          issues,
          "SPEF-A102",
          @diagnostic.Error,
          "connection references missing node id \{connection.node_id}",
          "ensure the connection node is interned in the same network",
          connection.span,
          net=net.name,
        )
      }
    }
    for resistor in net.resistors {
      if !valid_node(net, resistor.from_node) ||
        !valid_node(net, resistor.to_node) {
        add_issue(
          issues,
          "SPEF-A103",
          @diagnostic.Error,
          "resistor \{resistor.id} references a missing node",
          "correct both resistor endpoints",
          resistor.span,
          net=net.name,
        )
      }
    }
    for capacitor in net.capacitors {
      let missing_second = match capacitor.to_node {
        Some(id) => !valid_node(net, id)
        None => false
      }
      if !valid_node(net, capacitor.from_node) || missing_second {
        add_issue(
          issues,
          "SPEF-A104",
          @diagnostic.Error,
          "capacitor \{capacitor.id} references a missing node",
          "correct the capacitor endpoint reference",
          capacitor.span,
          net=net.name,
        )
      }
    }
  }
}

///|
fn check_rc_values(
  network : @model.RCNetwork,
  issues : Array[@diagnostic.Diagnostic],
  tolerance : Double,
) -> Unit {
  for net in network.nets {
    if invalid_number(net.declared_capacitance) ||
      net.declared_capacitance < 0.0 {
      add_issue(
        issues,
        "SPEF-A200",
        @diagnostic.Error,
        "network \{net.name} has invalid declared capacitance",
        "use a finite, non-negative total capacitance",
        net.span,
        net=net.name,
      )
    }
    for resistor in net.resistors {
      if invalid_number(resistor.resistance) || resistor.resistance < 0.0 {
        add_issue(
          issues,
          "SPEF-A201",
          @diagnostic.Error,
          "resistor \{resistor.id} has invalid resistance",
          "use a finite, non-negative resistance",
          resistor.span,
          net=net.name,
        )
      }
    }
    for capacitor in net.capacitors {
      if invalid_number(capacitor.capacitance) || capacitor.capacitance < 0.0 {
        add_issue(
          issues,
          "SPEF-A202",
          @diagnostic.Error,
          "capacitor \{capacitor.id} has invalid capacitance",
          "use a finite, non-negative capacitance",
          capacitor.span,
          net=net.name,
        )
      }
    }
    for left in 0..
      magnitude(detailed) {
      magnitude(net.declared_capacitance)
    } else {
      magnitude(detailed)
    }
    let relative_error = if denominator == 0.0 {
      0.0
    } else {
      magnitude(detailed - net.declared_capacitance) / denominator
    }
    if relative_error > tolerance {
      add_issue(
        issues,
        "SPEF-A205",
        @diagnostic.Warning,
        "network \{net.name} declared capacitance differs from *CAP sum",
        "verify extraction units and coupling-capacitance accounting",
        net.span,
        net=net.name,
      )
    }
  }
}

///|
fn check_topology(
  network : @model.RCNetwork,
  issues : Array[@diagnostic.Diagnostic],
) -> Unit {
  for net in network.nets {
    let topology = @graph.summarize(net)
    if topology.connected_components > 1 {
      add_issue(
        issues,
        "SPEF-A300",
        @diagnostic.Error,
        "network \{net.name} has \{topology.connected_components} disconnected components",
        "connect the resistance components or verify node naming",
        net.span,
        net=net.name,
      )
    }
    if topology.isolated_nodes.length() > 0 {
      add_issue(
        issues,
        "SPEF-A301",
        @diagnostic.Warning,
        "network \{net.name} has \{topology.isolated_nodes.length()} isolated nodes",
        "add missing resistance paths or remove unused nodes",
        net.span,
        net=net.name,
      )
    }
    if topology.has_cycle {
      add_issue(
        issues,
        "SPEF-A302",
        @diagnostic.Warning,
        "network \{net.name} contains a resistance cycle",
        "Elmore tree delay is unavailable; inspect extraction topology",
        net.span,
        net=net.name,
      )
    }
    if topology.driver_nodes.length() == 0 {
      add_issue(
        issues,
        "SPEF-A303",
        @diagnostic.Warning,
        "network \{net.name} has no output-direction driver",
        "mark one *CONN entry as output when appropriate",
        net.span,
        net=net.name,
      )
    } else if topology.driver_nodes.length() > 1 {
      add_issue(
        issues,
        "SPEF-A304",
        @diagnostic.Warning,
        "network \{net.name} has multiple output-direction drivers",
        "verify direction metadata before delay estimation",
        net.span,
        net=net.name,
      )
    }
  }
}

///|
fn check_complexity(
  network : @model.RCNetwork,
  issues : Array[@diagnostic.Diagnostic],
  threshold : Int,
) -> Unit {
  for net in network.nets {
    let metrics = @analyzer.analyze_net(net)
    if metrics.complexity_score > threshold {
      add_issue(
        issues,
        "SPEF-A400",
        @diagnostic.Warning,
        "network \{net.name} complexity score \{metrics.complexity_score} exceeds \{threshold}",
        "inspect this net separately or raise the configured threshold",
        net.span,
        net=net.name,
      )
    }
  }
}

///|
fn run_rule(
  rule : AuditRule,
  network : @model.RCNetwork,
  config : AuditConfig,
  issues : Array[@diagnostic.Diagnostic],
) -> Unit {
  match rule {
    HeaderRule => check_headers(network, issues)
    NameMapRule => check_name_map(network, issues)
    NetIntegrityRule => check_net_integrity(network, issues)
    RcValueRule =>
      check_rc_values(network, issues, config.capacitance_relative_tolerance)
    TopologyRule => check_topology(network, issues)
    ComplexityRule =>
      check_complexity(network, issues, config.complexity_threshold)
  }
}

///|
pub fn audit_with_rules(
  network : @model.RCNetwork,
  rules : Array[AuditRule],
  config? : AuditConfig = AuditConfig::default(),
) -> AuditReport {
  let issues = network.diagnostics.copy()
  for rule in rules {
    run_rule(rule, network, config, issues)
  }
  let mut errors = 0
  let mut warnings = 0
  let mut infos = 0
  for index in 0.. errors += 1
      @diagnostic.Warning => warnings += 1
      @diagnostic.Info => infos += 1
    }
  }
  {
    diagnostics: issues,
    error_count: errors,
    warning_count: warnings,
    info_count: infos,
    passed: errors == 0,
  }
}

///|
pub fn audit(
  network : @model.RCNetwork,
  config? : AuditConfig = AuditConfig::default(),
) -> AuditReport {
  audit_with_rules(network, default_rules(), config~)
}