///|
/// A maximal uncovered half-open interval for one required catalog metric.
pub(all) struct CatalogGap {
  metric : String
  starts_on : String
  ends_before : String
} derive(Debug, Eq)

///|
pub extend CatalogGap with Debug::{to_repr}

///|
pub extend CatalogGap with Eq::{equal, not_equal}

///|
/// Inspect every required metric over [starts_on, ends_before), without usage
/// samples or day-by-day expansion. Results follow catalog metric order, then
/// chronological order. Empty results mean complete date coverage only.
pub fn catalog_gaps(
  catalog : Catalog,
  starts_on : String,
  ends_before : String,
) -> Array[CatalogGap] raise PricingError {
  let start = date_key(starts_on, "coverage.starts_on")
  let end = date_key(ends_before, "coverage.ends_before")
  if end <= start {
    invalid("interval", "coverage", "end must be strictly after start")
  }
  let gaps : Array[CatalogGap] = []
  for metric in catalog.metrics {
    let mut cursor = start
    let mut cursor_text = starts_on
    for dated in catalog.entries {
      if tariff_info(dated.entry.tariff).metric != metric ||
        (dated.end is Some(n) && n <= cursor) {
        continue
      }
      if dated.start >= end {
        break
      }
      if dated.start > cursor {
        gaps.push({
          metric,
          starts_on: cursor_text,
          ends_before: dated.entry.starts_on,
        })
      }
      match dated.end {
        None => {
          cursor = end
          break
        }
        Some(n) => {
          if n >= end {
            cursor = end
            break
          }
          cursor = n
          cursor_text = dated.entry.ends_before.unwrap()
        }
      }
    }
    if cursor < end {
      gaps.push({ metric, starts_on: cursor_text, ends_before, })
    }
  }
  gaps
}