///|
/// A named database and its reusable field selection. Names are ASCII identifiers.
pub(all) struct EnrichmentSource {
  name : String
  reader : Reader
  fields : FieldSelector
}

///|
pub(all) enum SourceResult {
  Selected(Projection)
  Failed(MmdbError)
} derive(@debug.Debug)

///|
pub(all) struct Enrichment {
  sources : Array[(String, SourceResult)]
} derive(@debug.Debug)

///|
pub struct Enricher {
  priv sources : Array[EnrichmentSource]
}

///|
pub fn Enricher::new(
  sources : Array[EnrichmentSource],
) -> Enricher raise MmdbError {
  if sources.length() < 1 || sources.length() > 4 {
    raise MmdbError("source-limit", -1, "Expected one to four sources")
  }
  let seen : Map[String, Bool] = Map([])
  for source in sources {
    let name = source.name
    if name.length() < 1 || name.length() > 32 {
      raise MmdbError(
        "invalid-source", -1, "Source name must contain 1 to 32 ASCII characters",
      )
    }
    for i in 0..= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
      if !letter &&
        (i == 0 || !((ch >= '0' && ch <= '9') || ch == '_' || ch == '-')) {
        raise MmdbError(
          "invalid-source", -1, "Source name must start with a letter and contain letters, digits, _ or -",
        )
      }
    }
    if seen.contains(name) {
      raise MmdbError("duplicate-source", -1, "Duplicate source: " + name)
    }
    seen[name] = true
  }
  { sources: sources.copy(), }
}

///|
/// Invalid IP is a row error. Reader errors remain local to the named source.
pub fn Enricher::lookup(
  self : Enricher,
  ip : String,
) -> Enrichment raise MmdbError {
  ignore(parse_ip(ip))
  let sources = self.sources.map(source => {
    let result = Selected(source.reader.project_prepared(ip, source.fields)) catch {
      e => Failed(e)
    }
    (source.name, result)
  })
  { sources, }
}

///|
pub fn Enrichment::status_code(self : Enrichment) -> Int {
  let mut code = 0
  for item in self.sources {
    match item.1 {
      Failed(_) => code = 2
      Selected(p) => if !p.record_found { code = code.max(1) }
    }
  }
  code
}

///|
pub fn Enrichment::to_json(self : Enrichment) -> Json {
  let sources : Map[String, Json] = Map([])
  for item in self.sources {
    sources[item.0] = match item.1 {
      Selected(p) => p.to_json()
      Failed(e) => e.to_json()
    }
  }
  {
    "status": (match self.status_code() {
      2 => "error"
      1 => "not_found"
      _ => "found"
    }).to_json(),
    "sources": sources.to_json(),
  }
}