///|
/// One structured date field supplied by an ingest pipeline. The library
/// deliberately does not read CSV, MARC, JSON or network data itself.
pub(all) struct CatalogEntry {
  record_id : String
  edtf : String
} derive(Eq, @debug.Debug)

///|
/// A row may be invalid syntax, require a human judgment, or have a safe
/// envelope relationship to the requested exact-day window.
pub(all) enum CatalogDecision {
  Invalid
  NeedsReview
  OutsideWindow
  WithinWindow
  PossibleOverlapWindow
} derive(Eq, @debug.Debug)

///|
pub fn CatalogDecision::to_string(self : CatalogDecision) -> String {
  match self {
    Invalid => "invalid"
    NeedsReview => "needs-review"
    OutsideWindow => "outside"
    WithinWindow => "within"
    PossibleOverlapWindow => "possible-overlap"
  }
}

///|
/// An `Invalid` row has `error_offset` in the original UTF-16 input;
/// `NeedsReview` carries an unsupported-operation code with offset -1.
/// Rows with a window decision have a conservative envelope but no error.
pub(all) struct CatalogFinding {
  record_id : String
  input : String
  normalized : String
  decision : CatalogDecision
  error_code : String
  error_offset : Int
  envelope : DateEnvelope?
} derive(Eq, @debug.Debug)

///|
pub(all) struct CatalogReport {
  findings : Array[CatalogFinding]
  invalid : Int
  needs_review : Int
  outside : Int
  within : Int
  possible_overlap : Int
} derive(Eq, @debug.Debug)

///|
fn catalog_finding(
  entry : CatalogEntry,
  normalized : String,
  decision : CatalogDecision,
  code : String,
  offset : Int,
  envelope : DateEnvelope?,
) -> CatalogFinding {
  {
    record_id: entry.record_id,
    input: entry.edtf,
    normalized,
    decision,
    error_code: code,
    error_offset: offset,
    envelope,
  }
}

///|
fn analyze_catalog_entry(
  entry : CatalogEntry,
  start : CalendarDay,
  end : CalendarDay,
) -> CatalogFinding {
  let value = parse(entry.edtf) catch {
    Syntax(code, offset) =>
      return catalog_finding(entry, "", Invalid, code, offset, None)
    Unsupported(code, offset) =>
      return catalog_finding(entry, "", Invalid, code, offset, None)
  }
  let normalized = value.to_string()
  let envelope = date_envelope(value) catch {
    Unsupported(code, _) =>
      return catalog_finding(entry, normalized, NeedsReview, code, -1, None)
    Syntax(code, _) =>
      return catalog_finding(entry, normalized, NeedsReview, code, -1, None)
  }
  let decision = match relation_for_envelope(envelope, start, end) {
    Outside => OutsideWindow
    Within => WithinWindow
    PossibleOverlap => PossibleOverlapWindow
  }
  catalog_finding(entry, normalized, decision, "", -1, Some(envelope))
}

///|
/// Validate and screen a catalog batch against an inclusive exact-day window.
/// Invalid date fields are not silently dropped; uncertainty, approximation,
/// seasons, sets and open/unknown intervals go to `NeedsReview` rather than
/// receiving fabricated bounds. `PossibleOverlapWindow` is deliberately
/// conservative and can include false positives; it is not an exact match.
/// An invalid query window raises before any row is examined.
pub fn audit_catalog(
  entries : Array[CatalogEntry],
  query_start : String,
  query_end : String,
) -> CatalogReport raise EdtfError {
  let start = exact_query_day(parse(query_start))
  let end = exact_query_day(parse(query_end))
  if calendar_compare(start, end) > 0 {
    raise Unsupported("WINDOW_REVERSED", 0)
  }
  let findings : Array[CatalogFinding] = []
  let mut invalid = 0
  let mut needs_review = 0
  let mut outside = 0
  let mut within = 0
  let mut possible_overlap = 0
  for entry in entries {
    let finding = analyze_catalog_entry(entry, start, end)
    match finding.decision {
      Invalid => invalid += 1
      NeedsReview => needs_review += 1
      OutsideWindow => outside += 1
      WithinWindow => within += 1
      PossibleOverlapWindow => possible_overlap += 1
    }
    findings.push(finding)
  }
  { findings, invalid, needs_review, outside, within, possible_overlap, }
}