///|
/// A registry is MoonFolio's site-level view: many parsed documents become a
/// searchable and reviewable inventory of content folios.

///|
pub(all) struct DocumentSource {
  path : String
  text : String
} derive(Eq, Debug)

///|
pub(all) struct FolioRecord {
  path : String
  document : Document
  folio : ContentFolio
  diagnostics : Array[Diagnostic]
  labels : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct FolioRegistry {
  base_path : String
  records : Array[FolioRecord]
} derive(Eq, Debug)

///|
pub(all) struct FormatCount {
  format : MatterFormat
  count : Int
} derive(Eq, Debug)

///|
pub(all) struct TagCount {
  tag : String
  count : Int
} derive(Eq, Debug)

///|
pub(all) struct DuplicateValue {
  value : String
  paths : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct FormatBucket {
  format : MatterFormat
  records : Array[FolioRecord]
} derive(Eq, Debug)

///|
pub(all) struct TagBucket {
  tag : String
  records : Array[FolioRecord]
} derive(Eq, Debug)

///|
pub(all) struct RegistryFilter {
  tag : String
  path_prefix : String
  use_format : Bool
  format : MatterFormat
  use_draft : Bool
  draft : Bool
  valid_only : Bool
} derive(Eq, Debug)

///|
pub(all) struct RegistrySummary {
  total : Int
  valid : Int
  invalid : Int
  errors : Int
  warnings : Int
  drafts : Int
  published : Int
  unique_slugs : Int
  unique_fingerprints : Int
  duplicate_slugs : Array[DuplicateValue]
  duplicate_fingerprints : Array[DuplicateValue]
  formats : Array[FormatCount]
  tags : Array[TagCount]
  health_score : Int
} derive(Eq, Debug)

///|
pub fn registry_empty(base_path : String) -> FolioRegistry {
  { base_path, records: [] }
}

///|
pub fn registry_filter_all() -> RegistryFilter {
  {
    tag: "",
    path_prefix: "",
    use_format: false,
    format: None,
    use_draft: false,
    draft: false,
    valid_only: false,
  }
}

///|
pub fn registry_from_sources(
  sources : Array[DocumentSource],
  base_path : String,
  required : Array[String],
) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for source in sources {
    records.push(record_from_source(source, base_path, required))
  }
  { base_path, records }
}

///|
pub fn registry_from_documents(
  docs : Array[Document],
  base_path : String,
) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for i in 0.. FolioRecord {
  let output = parse_with_required(source.text, required)
  record_from_parse_output(source.path, output, base_path)
}

///|
pub fn record_from_parse_output(
  path : String,
  output : ParseOutput,
  base_path : String,
) -> FolioRecord {
  record_from_document(path, output.document, output.diagnostics, base_path)
}

///|
pub fn record_from_document(
  path : String,
  doc : Document,
  diagnostics : Array[Diagnostic],
  base_path : String,
) -> FolioRecord {
  let folio = build_folio(doc, base_path)
  {
    path,
    document: doc,
    folio,
    diagnostics: copy_diagnostics(diagnostics),
    labels: record_labels(doc),
  }
}

///|
pub fn registry_add_record(
  registry : FolioRegistry,
  record : FolioRecord,
) -> FolioRegistry {
  let records = copy_records(registry.records)
  records.push(record)
  { base_path: registry.base_path, records }
}

///|
pub fn registry_add_source(
  registry : FolioRegistry,
  source : DocumentSource,
  required : Array[String],
) -> FolioRegistry {
  registry_add_record(
    registry,
    record_from_source(source, registry.base_path, required),
  )
}

///|
pub fn registry_replace_record(
  registry : FolioRegistry,
  record : FolioRecord,
) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  let mut replaced = false
  for existing in registry.records {
    if existing.path == record.path {
      records.push(record)
      replaced = true
    } else {
      records.push(existing)
    }
  }
  if !replaced {
    records.push(record)
  }
  { base_path: registry.base_path, records }
}

///|
pub fn registry_remove_path(
  registry : FolioRegistry,
  path : String,
) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for record in registry.records {
    if record.path != path {
      records.push(record)
    }
  }
  { base_path: registry.base_path, records }
}

///|
pub fn registry_rebase(
  registry : FolioRegistry,
  base_path : String,
) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for record in registry.records {
    records.push(
      record_from_document(
        record.path,
        record.document,
        record.diagnostics,
        base_path,
      ),
    )
  }
  { base_path, records }
}

///|
pub fn registry_merge(
  left : FolioRegistry,
  right : FolioRegistry,
) -> FolioRegistry {
  let mut merged = {
    base_path: left.base_path,
    records: copy_records(left.records),
  }
  for record in right.records {
    merged = registry_replace_record(merged, record)
  }
  merged
}

///|
pub fn registry_contains_path(registry : FolioRegistry, path : String) -> Bool {
  registry.records.any(record => record.path == path)
}

///|
pub fn registry_find_by_path(
  registry : FolioRegistry,
  path : String,
) -> FolioRecord? {
  for record in registry.records {
    if record.path == path {
      return Some(record)
    }
  }
  None
}

///|
pub fn registry_find_by_slug(
  registry : FolioRegistry,
  slug : String,
) -> FolioRecord? {
  for record in registry.records {
    if record.folio.slug == slug {
      return Some(record)
    }
  }
  None
}

///|
pub fn registry_find_by_permalink(
  registry : FolioRegistry,
  permalink : String,
) -> FolioRecord? {
  for record in registry.records {
    if record.folio.permalink == permalink {
      return Some(record)
    }
  }
  None
}

///|
pub fn registry_find_by_fingerprint(
  registry : FolioRegistry,
  fingerprint : String,
) -> FolioRecord? {
  for record in registry.records {
    if record.folio.fingerprint == fingerprint {
      return Some(record)
    }
  }
  None
}

///|
pub fn registry_filter(
  registry : FolioRegistry,
  filter : RegistryFilter,
) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for record in registry.records {
    if record_matches_filter(record, filter) {
      records.push(record)
    }
  }
  { base_path: registry.base_path, records }
}

///|
pub fn registry_by_tag(registry : FolioRegistry, tag : String) -> FolioRegistry {
  let filter = registry_filter_all()
  registry_filter(registry, {
    tag: normalize_tag(tag),
    path_prefix: filter.path_prefix,
    use_format: filter.use_format,
    format: filter.format,
    use_draft: filter.use_draft,
    draft: filter.draft,
    valid_only: filter.valid_only,
  })
}

///|
pub fn registry_by_format(
  registry : FolioRegistry,
  format : MatterFormat,
) -> FolioRegistry {
  registry_filter(registry, {
    tag: "",
    path_prefix: "",
    use_format: true,
    format,
    use_draft: false,
    draft: false,
    valid_only: false,
  })
}

///|
pub fn registry_by_path_prefix(
  registry : FolioRegistry,
  prefix : String,
) -> FolioRegistry {
  registry_filter(registry, {
    tag: "",
    path_prefix: prefix,
    use_format: false,
    format: None,
    use_draft: false,
    draft: false,
    valid_only: false,
  })
}

///|
pub fn registry_drafts(registry : FolioRegistry) -> FolioRegistry {
  registry_filter(registry, {
    tag: "",
    path_prefix: "",
    use_format: false,
    format: None,
    use_draft: true,
    draft: true,
    valid_only: false,
  })
}

///|
pub fn registry_published(registry : FolioRegistry) -> FolioRegistry {
  registry_filter(registry, {
    tag: "",
    path_prefix: "",
    use_format: false,
    format: None,
    use_draft: true,
    draft: false,
    valid_only: false,
  })
}

///|
pub fn registry_valid(registry : FolioRegistry) -> FolioRegistry {
  registry_filter(registry, {
    tag: "",
    path_prefix: "",
    use_format: false,
    format: None,
    use_draft: false,
    draft: false,
    valid_only: true,
  })
}

///|
pub fn registry_invalid(registry : FolioRegistry) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for record in registry.records {
    if record_has_errors(record) {
      records.push(record)
    }
  }
  { base_path: registry.base_path, records }
}

///|
pub fn registry_with_warnings(registry : FolioRegistry) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for record in registry.records {
    if record_has_warnings(record) {
      records.push(record)
    }
  }
  { base_path: registry.base_path, records }
}

///|
pub fn registry_summary(registry : FolioRegistry) -> RegistrySummary {
  let total = registry.records.length()
  let mut valid = 0
  let mut invalid = 0
  let mut errors = 0
  let mut warnings = 0
  let mut drafts = 0
  let mut published = 0
  for record in registry.records {
    if record_has_errors(record) {
      invalid = invalid + 1
    } else {
      valid = valid + 1
    }
    errors = errors + count_error_diagnostics(record.diagnostics)
    warnings = warnings + count_warning_diagnostics(record.diagnostics)
    if record_is_draft(record) {
      drafts = drafts + 1
    } else {
      published = published + 1
    }
  }
  let duplicate_slugs = registry_duplicate_slugs(registry)
  let duplicate_fingerprints = registry_duplicate_fingerprints(registry)
  let formats = registry_format_counts(registry)
  let tags = registry_tag_counts(registry)
  let unique_slugs = registry_unique_slugs(registry).length()
  let unique_fingerprints = registry_unique_fingerprints(registry).length()
  let health_score = registry_health_score_parts(
    total,
    errors,
    warnings,
    duplicate_slugs.length(),
    duplicate_fingerprints.length(),
  )
  {
    total,
    valid,
    invalid,
    errors,
    warnings,
    drafts,
    published,
    unique_slugs,
    unique_fingerprints,
    duplicate_slugs,
    duplicate_fingerprints,
    formats,
    tags,
    health_score,
  }
}

///|
pub fn registry_health_score(registry : FolioRegistry) -> Int {
  registry_summary(registry).health_score
}

///|
pub fn registry_manifest(registry : FolioRegistry) -> String {
  let lines : Array[String] = []
  for record in registry.records {
    lines.push(record.folio.manifest_line)
  }
  lines.join("\n")
}

///|
pub fn registry_manifest_sorted(registry : FolioRegistry) -> String {
  let records = registry_sort_by_slug(registry).records
  let lines : Array[String] = []
  for record in records {
    lines.push(record.folio.manifest_line)
  }
  lines.join("\n")
}

///|
pub fn registry_paths(registry : FolioRegistry) -> Array[String] {
  let paths : Array[String] = []
  for record in registry.records {
    paths.push(record.path)
  }
  paths
}

///|
pub fn registry_slugs(registry : FolioRegistry) -> Array[String] {
  let slugs : Array[String] = []
  for record in registry.records {
    slugs.push(record.folio.slug)
  }
  slugs
}

///|
pub fn registry_titles(registry : FolioRegistry) -> Array[String] {
  let titles : Array[String] = []
  for record in registry.records {
    titles.push(record.folio.title)
  }
  titles
}

///|
pub fn registry_permalinks(registry : FolioRegistry) -> Array[String] {
  let values : Array[String] = []
  for record in registry.records {
    values.push(record.folio.permalink)
  }
  values
}

///|
pub fn registry_unique_tags(registry : FolioRegistry) -> Array[String] {
  let tags : Array[String] = []
  for record in registry.records {
    for tag in record.labels {
      if !contains_string(tags, tag) {
        tags.push(tag)
      }
    }
  }
  sorted_strings(tags)
}

///|
pub fn registry_unique_slugs(registry : FolioRegistry) -> Array[String] {
  let slugs : Array[String] = []
  for record in registry.records {
    if !contains_string(slugs, record.folio.slug) {
      slugs.push(record.folio.slug)
    }
  }
  sorted_strings(slugs)
}

///|
pub fn registry_unique_fingerprints(registry : FolioRegistry) -> Array[String] {
  let values : Array[String] = []
  for record in registry.records {
    if !contains_string(values, record.folio.fingerprint) {
      values.push(record.folio.fingerprint)
    }
  }
  sorted_strings(values)
}

///|
pub fn registry_duplicate_slugs(
  registry : FolioRegistry,
) -> Array[DuplicateValue] {
  duplicates_from_records(registry.records, duplicate_key_slug)
}

///|
pub fn registry_duplicate_fingerprints(
  registry : FolioRegistry,
) -> Array[DuplicateValue] {
  duplicates_from_records(registry.records, duplicate_key_fingerprint)
}

///|
pub fn registry_format_counts(registry : FolioRegistry) -> Array[FormatCount] {
  let counts : Array[FormatCount] = []
  for record in registry.records {
    increment_format_count(counts, record.document.format)
  }
  counts
}

///|
pub fn registry_tag_counts(registry : FolioRegistry) -> Array[TagCount] {
  let counts : Array[TagCount] = []
  for record in registry.records {
    for tag in record.labels {
      increment_tag_count(counts, tag)
    }
  }
  sort_tag_counts(counts)
}

///|
pub fn registry_group_by_format(
  registry : FolioRegistry,
) -> Array[FormatBucket] {
  let buckets : Array[FormatBucket] = []
  for record in registry.records {
    push_format_bucket(buckets, record)
  }
  buckets
}

///|
pub fn registry_group_by_tag(registry : FolioRegistry) -> Array[TagBucket] {
  let buckets : Array[TagBucket] = []
  for tag in registry_unique_tags(registry) {
    let records : Array[FolioRecord] = []
    for record in registry.records {
      if contains_string(record.labels, tag) {
        records.push(record)
      }
    }
    buckets.push({ tag, records })
  }
  buckets
}

///|
pub fn registry_sort_by_slug(registry : FolioRegistry) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for record in registry.records {
    insert_record_by_slug(records, record)
  }
  { base_path: registry.base_path, records }
}

///|
pub fn registry_sort_by_title(registry : FolioRegistry) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for record in registry.records {
    insert_record_by_title(records, record)
  }
  { base_path: registry.base_path, records }
}

///|
pub fn registry_sort_by_path(registry : FolioRegistry) -> FolioRegistry {
  let records : Array[FolioRecord] = []
  for record in registry.records {
    insert_record_by_path(records, record)
  }
  { base_path: registry.base_path, records }
}

///|
pub fn registry_stable_ids(registry : FolioRegistry) -> Array[String] {
  let ids : Array[String] = []
  for record in registry.records {
    ids.push(record_stable_id(record))
  }
  ids
}

///|
pub fn record_stable_id(record : FolioRecord) -> String {
  record.path + "|" + record.folio.slug + "|" + record.folio.fingerprint
}

///|
pub fn record_is_draft(record : FolioRecord) -> Bool {
  get_bool(record.document, "draft").unwrap_or(false)
}

///|
pub fn record_has_errors(record : FolioRecord) -> Bool {
  count_error_diagnostics(record.diagnostics) > 0
}

///|
pub fn record_has_warnings(record : FolioRecord) -> Bool {
  count_warning_diagnostics(record.diagnostics) > 0
}

///|
pub fn record_label_line(record : FolioRecord) -> String {
  if record.labels.length() == 0 {
    "(none)"
  } else {
    record.labels.join(",")
  }
}

///|
pub fn registry_table(registry : FolioRegistry) -> String {
  let lines : Array[String] = ["path | slug | status | labels"]
  for record in registry_sort_by_path(registry).records {
    let status = if record_has_errors(record) {
      "error"
    } else if record_has_warnings(record) {
      "warning"
    } else {
      "ok"
    }
    lines.push(
      record.path +
      " | " +
      record.folio.slug +
      " | " +
      status +
      " | " +
      record_label_line(record),
    )
  }
  lines.join("\n")
}

///|
fn record_matches_filter(record : FolioRecord, filter : RegistryFilter) -> Bool {
  if !filter.tag.is_empty() && !contains_string(record.labels, filter.tag) {
    return false
  }
  if !filter.path_prefix.is_empty() &&
    !record.path.has_prefix(filter.path_prefix) {
    return false
  }
  if filter.use_format && record.document.format != filter.format {
    return false
  }
  if filter.use_draft && record_is_draft(record) != filter.draft {
    return false
  }
  if filter.valid_only && record_has_errors(record) {
    return false
  }
  true
}

///|
fn record_labels(doc : Document) -> Array[String] {
  match get_list(doc, "tags") {
    Some(tags) => normalize_tags(tags)
    None => []
  }
}

///|
fn copy_records(records : Array[FolioRecord]) -> Array[FolioRecord] {
  let result : Array[FolioRecord] = []
  for record in records {
    result.push(record)
  }
  result
}

///|
fn copy_diagnostics(diagnostics : Array[Diagnostic]) -> Array[Diagnostic] {
  let result : Array[Diagnostic] = []
  for diagnostic in diagnostics {
    result.push(diagnostic)
  }
  result
}

///|
fn duplicate_key_slug(record : FolioRecord) -> String {
  record.folio.slug
}

///|
fn duplicate_key_fingerprint(record : FolioRecord) -> String {
  record.folio.fingerprint
}

///|
fn duplicates_from_records(
  records : Array[FolioRecord],
  get_key : (FolioRecord) -> String,
) -> Array[DuplicateValue] {
  let values : Array[String] = []
  for record in records {
    let key = get_key(record)
    if !contains_string(values, key) {
      values.push(key)
    }
  }
  let duplicates : Array[DuplicateValue] = []
  for value in values {
    let paths : Array[String] = []
    for record in records {
      if get_key(record) == value {
        paths.push(record.path)
      }
    }
    if paths.length() > 1 {
      duplicates.push({ value, paths })
    }
  }
  duplicates
}

///|
fn increment_format_count(
  counts : Array[FormatCount],
  format : MatterFormat,
) -> Unit {
  for i in 0.. Unit {
  for i in 0.. Array[TagCount] {
  let sorted : Array[TagCount] = []
  for item in counts {
    insert_tag_count(sorted, item)
  }
  sorted
}

///|
fn push_format_bucket(
  buckets : Array[FormatBucket],
  record : FolioRecord,
) -> Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Int {
  if total == 0 {
    return 0
  }
  let score = 100 -
    errors * 18 -
    warnings * 6 -
    duplicate_slugs * 12 -
    duplicate_fingerprints * 8
  clamp_int(score, 0, 100)
}