///|
/// Scalar S-parameter metrics. Losses use -20 log10(|S|), not power dB.
/// Reflection/return loss/VSWR use S[input,input]; transmission/insertion
/// loss use S[output,input]. Other ports are matched at their real references.
pub(all) enum BandMetric {
  ReflectionMagnitude
  TransmissionMagnitude
  ReturnLossDb
  InsertionLossDb
  Vswr
}

///|
pub fn BandMetric::name(self : BandMetric) -> String {
  match self {
    ReflectionMagnitude => "reflection_magnitude"
    TransmissionMagnitude => "transmission_magnitude"
    ReturnLossDb => "return_loss_db"
    InsertionLossDb => "insertion_loss_db"
    Vswr => "vswr"
  }
}

///|
/// A validated, immutable rule over a CLOSED frequency interval in Hz.
/// Limits are inclusive, finite and exact (no implicit tolerance).
pub struct BandLimit {
  metric : BandMetric
  start_hz : Double
  end_hz : Double
  input : Int
  output : Int
  minimum : Double?
  maximum : Double?
}

///|
fn validate_band_domain(
  start_hz : Double,
  end_hz : Double,
  input : Int,
  output : Int,
) -> Unit raise {
  if !finite(start_hz) || !finite(end_hz) || start_hz < 0.0 || end_hz < start_hz {
    raise Failure("band edges must be finite with 0 <= start_hz <= end_hz")
  }
  if input < 0 || input >= 32 || output < 0 || output >= 32 {
    raise Failure("band ports must be zero-based indices in 0..31")
  }
}

///|
/// At least one bound is required. Equal frequency edges permit a single-point
/// check. Port existence is additionally checked against the target network.
pub fn band_limit(
  metric : BandMetric,
  start_hz : Double,
  end_hz : Double,
  input~ : Int,
  output~ : Int,
  minimum? : Double,
  maximum? : Double,
) -> BandLimit raise {
  validate_band_domain(start_hz, end_hz, input, output)
  if minimum is None && maximum is None {
    raise Failure("band rule requires a minimum or maximum")
  }
  if minimum is Some(x) && !finite(x) {
    raise Failure("band minimum must be finite")
  }
  if maximum is Some(x) && !finite(x) {
    raise Failure("band maximum must be finite")
  }
  if minimum is Some(lo) && maximum is Some(hi) && lo > hi {
    raise Failure("band minimum exceeds maximum")
  }
  { metric, start_hz, end_hz, input, output, minimum, maximum, }
}

///|
/// Source index is zero-based in the ORIGINAL network, not the selected band.
/// state: finite, infinite (+infinity), or undefined (active-reflection VSWR).
/// JSON value is an explicit number/null, never a one-element Option array.
pub struct BandObservation {
  point : Int
  frequency_hz : Double
  value : Double?
  state : String
}

///|
fn nullable_number(value : Double?) -> Json {
  match value {
    Some(x) => x.to_json()
    None => null
  }
}

///|
pub impl ToJson for BandObservation with fn to_json(self) {
  {
    "point": self.point.to_json(),
    "frequency_hz": self.frequency_hz.to_json(),
    "value": nullable_number(self.value),
    "state": self.state.to_json(),
  }
}

///|
pub extend BandObservation with ToJson::{to_json}

///|
/// reason: below_minimum, above_maximum, or undefined. Issues are ordered by
/// rule then original frequency; they are NOT contiguous violation intervals.
pub struct BandIssue {
  observation : BandObservation
  reason : String
} derive(ToJson)

///|
pub extend BandIssue with ToJson::{to_json}

///|
/// pass means all selected SAMPLES passed, with nonempty selection, dataset
/// range covering both requested edges and no undefined values. It does NOT
/// certify the unsampled frequencies, even when both edge_sampled flags hold.
/// fail takes precedence over inconclusive. Undefined values are not extrema.
/// Repeated extrema retain the first original point. Counts are never capped.
pub struct BandResult {
  rule : BandLimit
  status : String
  range_covered : Bool
  start_sampled : Bool
  end_sampled : Bool
  sample_count : Int
  failed_count : Int
  undefined_count : Int
  lowest : BandObservation?
  highest : BandObservation?
  issues : Array[BandIssue]
  issues_truncated : Bool
}

///|
pub impl ToJson for BandResult with fn to_json(self) {
  {
    "metric": self.rule.metric.name().to_json(),
    "start_hz": self.rule.start_hz.to_json(),
    "end_hz": self.rule.end_hz.to_json(),
    "input": self.rule.input.to_json(),
    "output": self.rule.output.to_json(),
    "minimum": nullable_number(self.rule.minimum),
    "maximum": nullable_number(self.rule.maximum),
    "status": self.status.to_json(),
    "range_covered": self.range_covered.to_json(),
    "start_sampled": self.start_sampled.to_json(),
    "end_sampled": self.end_sampled.to_json(),
    "sample_count": self.sample_count.to_json(),
    "failed_count": self.failed_count.to_json(),
    "undefined_count": self.undefined_count.to_json(),
    "lowest": match self.lowest {
      Some(x) => x.to_json()
      None => null
    },
    "highest": match self.highest {
      Some(x) => x.to_json()
      None => null
    },
    "issues": self.issues.to_json(),
    "issues_truncated": self.issues_truncated.to_json(),
  }
}

///|
pub extend BandResult with ToJson::{to_json}

///|
/// Aggregate sampled-only verdict, preserving the original reference ohms.
/// fail if ANY rule fails; else inconclusive if ANY rule is inconclusive.
/// Details share ONE global max_details budget, consumed in rule/point order.
pub struct BandReport {
  scope : String
  status : String
  reference_ohms : Array[Double]
  max_details : Int
  results : Array[BandResult]
} derive(ToJson)

///|
pub extend BandReport with ToJson::{to_json}

///|
// Returns [lo,hi) indices selecting the closed [start,end] frequency interval.
// Strictly increasing finite frequencies are guaranteed by Network.
fn band_indices(frequencies : Array[Double], rule : BandLimit) -> (Int, Int) {
  let lo = match frequencies.binary_search(rule.start_hz) {
    Ok(i) | Err(i) => i
  }
  let hi = match frequencies.binary_search(rule.end_hz) {
    Ok(i) => i + 1
    Err(i) => i
  }
  (lo, hi)
}

///|
// None is +infinity here; undefined observations never enter comparisons.
fn observation_less(a : BandObservation, b : BandObservation) -> Bool {
  match (a.value, b.value) {
    (Some(x), Some(y)) => x < y
    (Some(_), None) => true
    _ => false
  }
}

///|
fn band_observation(
  s : Network,
  rule : BandLimit,
  point : Int,
) -> BandObservation raise {
  let column = rule.input
  let row = match rule.metric {
    TransmissionMagnitude | InsertionLossDb => rule.output
    _ => rule.input
  }
  let magnitude = s.values[point][row * s.n + column].magnitude()
  if !finite(magnitude) {
    raise Failure("band metric magnitude overflow")
  }
  let (value, state) : (Double?, String) = match rule.metric {
    ReflectionMagnitude | TransmissionMagnitude => (Some(magnitude), "finite")
    ReturnLossDb | InsertionLossDb =>
      if magnitude == 0.0 {
        (None, "infinite")
      } else {
        (Some(-20.0 * @math.log10(magnitude)), "finite")
      }
    Vswr =>
      if magnitude > 1.0 {
        (None, "undefined")
      } else if magnitude == 1.0 {
        (None, "infinite")
      } else {
        (Some((1.0 + magnitude) / (1.0 - magnitude)), "finite")
      }
  }
  { point, frequency_hz: s.frequencies[point], value, state, }
}

///|
fn check_band(
  s : Network,
  rule : BandLimit,
  lo : Int,
  hi : Int,
  budget : Int,
) -> BandResult raise {
  let mut lowest : BandObservation? = None
  let mut highest : BandObservation? = None
  let mut failed_count = 0
  let mut undefined_count = 0
  let issues : Array[BandIssue] = []
  for point in lo.. bound)
        ) {
        "above_maximum"
      } else {
        ""
      }
    }
    if reason != "" {
      if reason != "undefined" {
        failed_count += 1
      }
      if issues.length() < budget {
        issues.push({ observation, reason, })
      }
    }
  }
  let range_covered = s.frequencies[0] <= rule.start_hz &&
    s.frequencies[s.frequencies.length() - 1] >= rule.end_hz
  let status = if failed_count > 0 {
    "fail"
  } else if hi == lo || !range_covered || undefined_count > 0 {
    "inconclusive"
  } else {
    "pass"
  }
  {
    rule,
    status,
    range_covered,
    start_sampled: lo < hi && s.frequencies[lo] == rule.start_hz,
    end_sampled: lo < hi && s.frequencies[hi - 1] == rule.end_hz,
    sample_count: hi - lo,
    failed_count,
    undefined_count,
    lowest,
    highest,
    issues,
    issues_truncated: issues.length() < failed_count + undefined_count,
  }
}

///|
/// Check 1..256 rules on ORIGINAL sampled frequencies only. No interpolation,
/// extrapolation, resampling, or automatic reference renormalization. Convert
/// the network to S once; a singular conversion fails the whole operation.
/// Limit total selected point/rule evaluations to 2,000,000. Detail budget is
/// 0..100000 (default 256); truncation never changes verdicts/counts/extrema.
pub fn Network::check_bands(
  self : Network,
  limits : Array[BandLimit],
  max_details? : Int = 256,
) -> BandReport raise {
  if limits.is_empty() || limits.length() > 256 {
    raise Failure("band check requires 1..256 rules")
  }
  if max_details < 0 || max_details > 100000 {
    raise Failure("max_details must be in 0..100000")
  }
  let ranges : Array[(Int, Int)] = []
  let mut evaluations = 0
  for rule in limits {
    check_ports(self.n, rule.output, rule.input)
    let (lo, hi) = band_indices(self.frequencies, rule)
    evaluations += hi - lo
    if evaluations > 2000000 {
      raise Failure("band checks exceed 2000000 point/rule evaluations")
    }
    ranges.push((lo, hi))
  }
  let s = self.convert("S")
  let results : Array[BandResult] = []
  let mut budget = max_details
  let mut status = "pass"
  for i, rule in limits {
    let (lo, hi) = ranges[i]
    let result = check_band(s, rule, lo, hi, budget)
    budget -= result.issues.length()
    if result.status == "fail" {
      status = "fail"
    } else if result.status == "inconclusive" && status == "pass" {
      status = "inconclusive"
    }
    results.push(result)
  }
  {
    scope: "sampled_points_only",
    status,
    reference_ohms: self.reference.copy(),
    max_details,
    results,
  }
}

///|
/// Export retained issues only. An empty CSV is NOT a pass certificate: use
/// the JSON report for uncovered bands, counts, verdicts and truncation flags.
/// All strings are fixed library identifiers; no user text enters CSV cells.
pub fn BandReport::issues_csv(self : BandReport) -> String {
  let out = StringBuilder()
  out.write_string(
    "rule,metric,input,output,point,frequency_hz,value,state,reason\n",
  )
  for i, result in self.results {
    for issue in result.issues {
      let p = issue.observation
      let value = match p.value {
        Some(x) => x.to_string()
        None => ""
      }
      out.write_string(
        "\{i},\{result.rule.metric.name()},\{result.rule.input},\{result.rule.output},\{p.point},\{p.frequency_hz},\{value},\{p.state},\{issue.reason}\n",
      )
    }
  }
  out.to_string()
}