///|
/// A frontmatter key that a profile defines.
pub(all) struct KeyDefinition {
  key : FieldKey
  /// The allowed values, or empty for any value.
  values : ReadOnlyArray[String]
  description : String
} derive(Eq, Debug)

///|
pub fn KeyDefinition::KeyDefinition(
  key : FieldKey,
  values~ : ArrayView[String] = [],
  description~ : String = "",
) -> KeyDefinition {
  { key, values: ReadOnlyArray::from_array(values.to_owned()), description, }
}

///|
/// A profile extends OKF with conventions: vocabulary, derived metadata,
/// registers, references, templates and rules. Every method has a default, so
/// a profile implements only what it adds. The methods that define keys,
/// registers, references, templates, rules and severities can raise
/// `ValueError`; validation then reports the profile as invalid.
pub(open) trait Profile {
  /// The profile name, used in reports.
  fn name(Self) -> String
  /// Concept frontmatter keys that this profile defines.
  fn vocabulary(Self) -> Array[KeyDefinition] raise ValueError = _
  /// Root index frontmatter keys that turn on profile behavior for a bundle.
  fn bundle_keys(Self) -> Array[KeyDefinition] raise ValueError = _
  /// Metadata for a document without frontmatter, from its path and body.
  fn derive_metadata(Self, Document) -> Fields? = _
  /// Typed views of concepts of one type.
  fn registers(Self) -> Array[Register] raise ValueError = _
  /// Frontmatter fields whose values name documents.
  fn reference_fields(Self) -> Array[ReferenceField] raise ValueError = _
  /// Templates for new concepts.
  fn templates(Self) -> Array[Template] raise ValueError = _
  /// Rule sets that validation runs.
  fn rule_sets(Self) -> Array[RuleSet] raise ValueError = _
  /// Options that this profile prefers. The caller's options win.
  fn options(Self) -> Options? = _
  /// Links that the profile adds to the graph for a document, such as links
  /// that its path or frontmatter implies.
  fn relations(Self, Bundle, Document) -> Array[Link] = _
  /// Rule severities that this profile prefers. The caller's config wins.
  fn severities(Self) -> Array[(RuleName, Severity)] raise ValueError = _
}

///|
impl Profile with fn relations(_, _, _) {
  []
}

///|
impl Profile with fn vocabulary(_) {
  []
}

///|
impl Profile with fn bundle_keys(_) {
  []
}

///|
impl Profile with fn derive_metadata(_, _) {
  None
}

///|
impl Profile with fn registers(_) {
  []
}

///|
impl Profile with fn reference_fields(_) {
  []
}

///|
impl Profile with fn templates(_) {
  []
}

///|
impl Profile with fn rule_sets(_) {
  []
}

///|
impl Profile with fn options(_) {
  None
}

///|
/// The first derived metadata from `profiles`, in order.
fn derive_with(profiles : ArrayView[&Profile], doc : Document) -> Fields? {
  for profile in profiles {
    match profile.derive_metadata(doc) {
      Some(fields) => return Some(fields)
      None => ()
    }
  }
  None
}

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

///|
pub extend KeyDefinition with @debug.Debug::{to_repr}

///|
/// Two profiles that define the same key or register differently.
pub(all) enum ProfileConflict {
  /// Two profiles define the frontmatter key with different values.
  KeyConflict(key~ : FieldKey, first~ : String, second~ : String)
  /// Two profiles define a register for the same concept type differently.
  RegisterConflict(
    concept_type~ : ConceptType,
    first~ : String,
    second~ : String
  )
  /// A profile definition breaks a rule of a value type.
  ProfileInvalid(profile~ : String, error~ : ValueError)
} derive(Eq, Debug)

///|
fn same_register(a : Register, b : Register) -> Bool {
  a.name == b.name &&
  a.id_rule == b.id_rule &&
  a.state_key == b.state_key &&
  a.states == b.states &&
  a.supersedes_key == b.supersedes_key &&
  a.superseded_by_key == b.superseded_by_key &&
  a.bundle_flag == b.bundle_flag
}

///|
/// The conflicts between `profiles`, and the profiles whose definitions break
/// a rule. The library never selects one definition in silence; a caller
/// reports the conflicts or removes a profile.
pub fn profile_conflicts(
  profiles : ArrayView[&Profile],
) -> Array[ProfileConflict] {
  let out = []
  let keys : Map[FieldKey, (String, KeyDefinition)] = Map([])
  let registers : Map[ConceptType, (String, Register)] = Map([])
  let register_names : Array[RegisterName] = []
  let references : Array[(String, ReferenceField)] = []
  for profile in profiles {
    let name = profile.name()
    try {
      for def in profile.vocabulary() {
        match keys.get(def.key) {
          Some((first, other)) if other.values != def.values =>
            out.push(KeyConflict(key=def.key, first~, second=name))
          Some(_) => ()
          None => keys[def.key] = (name, def)
        }
      }
      for register in profile.registers() {
        register_names.push(register.name)
        match registers.get(register.concept_type) {
          Some((first, other)) if !same_register(other, register) =>
            out.push(
              RegisterConflict(
                concept_type=register.concept_type,
                first~,
                second=name,
              ),
            )
          Some(_) => ()
          None => registers[register.concept_type] = (name, register)
        }
      }
      for field in profile.reference_fields() {
        references.push((name, field))
      }
      profile.bundle_keys() |> ignore
      profile.templates() |> ignore
      profile.rule_sets() |> ignore
      profile.severities() |> ignore
    } catch {
      e => out.push(ProfileInvalid(profile=name, error=e))
    }
  }
  // A reference field must name a register that some profile defines.
  for pair in references {
    match pair.1.kind {
      RecordRef(register~) if !register_names.contains(register) =>
        out.push(
          ProfileInvalid(
            profile=pair.0,
            error=UnknownRegister(register=register.to_string()),
          ),
        )
      _ => ()
    }
  }
  out
}

///|
/// The options to load with: the caller's options when given, else the
/// options of the first profile that has some, else the OKF v0.2 defaults.
pub fn effective_options(
  profiles : ArrayView[&Profile],
  caller? : Options,
) -> Options {
  match caller {
    Some(o) => o
    None => {
      for profile in profiles {
        match profile.options() {
          Some(o) => return o
          None => ()
        }
      }
      Options::default()
    }
  }
}

///|
/// Every register of `profiles`. A profile whose registers raise adds none;
/// `validate` reports it, and the registers of the other profiles stay.
pub fn registers_of(profiles : ArrayView[&Profile]) -> Array[Register] {
  let out = []
  for profile in profiles {
    let registers = profile.registers() catch { _ => [] }
    for r in registers {
      out.push(r)
    }
  }
  out
}

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

///|
pub extend ProfileConflict with @debug.Debug::{to_repr}

///|
impl Profile with fn severities(_) {
  []
}