///|
/// Content profile checks turn a parsed metadata block into a readable
/// contract. They intentionally work above Markdown parsing: MoonFolio cares
/// about whether a content folio is publishable, stable, and maintainable.

///|
pub(all) enum ProfileFindingKind {
  MissingRequired
  ForbiddenField
  BlankText
  TextTooLong
  TagNotAllowed
  TagRequired
  TooManyTags
  DraftBlocked
  DraftNeedsApproval
  PermalinkMissing
  PermalinkPrefixMismatch
  DateMissing
  DateInvalid
  DateOutOfRange
} derive(Eq, Debug)

///|
pub(all) struct ProfileFinding {
  kind : ProfileFindingKind
  key : String
  value : String
  message : String
  severity : Severity
} derive(Eq, Debug)

///|
pub(all) struct ContentProfile {
  required_fields : Array[String]
  forbidden_fields : Array[String]
  title_key : String
  max_title_length : Int
  summary_key : String
  require_summary : Bool
  max_summary_length : Int
  draft_key : String
  allow_draft : Bool
  require_approval_for_draft : Bool
  approval_key : String
  tag_key : String
  allowed_tags : Array[String]
  required_tags : Array[String]
  max_tags : Int
  permalink_key : String
  require_permalink : Bool
  permalink_prefix : String
  date_key : String
  require_date : Bool
  min_year : Int
  max_year : Int
} derive(Eq, Debug)

///|
pub(all) struct ProfileReport {
  ok : Bool
  score : Int
  checked : Array[String]
  findings : Array[ProfileFinding]
  diagnostics : Array[Diagnostic]
  suggestions : Array[String]
} derive(Eq, Debug)

///|
pub fn default_content_profile() -> ContentProfile {
  {
    required_fields: ["title"],
    forbidden_fields: [],
    title_key: "title",
    max_title_length: 80,
    summary_key: "summary",
    require_summary: false,
    max_summary_length: 180,
    draft_key: "draft",
    allow_draft: true,
    require_approval_for_draft: false,
    approval_key: "approved",
    tag_key: "tags",
    allowed_tags: [],
    required_tags: [],
    max_tags: 8,
    permalink_key: "permalink",
    require_permalink: false,
    permalink_prefix: "",
    date_key: "date",
    require_date: false,
    min_year: 1970,
    max_year: 9999,
  }
}

///|
pub fn strict_content_profile() -> ContentProfile {
  {
    required_fields: ["title", "summary", "date", "tags"],
    forbidden_fields: ["todo", "tmp", "placeholder"],
    title_key: "title",
    max_title_length: 72,
    summary_key: "summary",
    require_summary: true,
    max_summary_length: 160,
    draft_key: "draft",
    allow_draft: false,
    require_approval_for_draft: true,
    approval_key: "approved",
    tag_key: "tags",
    allowed_tags: [],
    required_tags: [],
    max_tags: 6,
    permalink_key: "permalink",
    require_permalink: true,
    permalink_prefix: "/",
    date_key: "date",
    require_date: true,
    min_year: 2020,
    max_year: 2100,
  }
}

///|
pub fn profile_with_allowed_tags(
  profile : ContentProfile,
  tags : Array[String],
) -> ContentProfile {
  {
    required_fields: copy_strings(profile.required_fields),
    forbidden_fields: copy_strings(profile.forbidden_fields),
    title_key: profile.title_key,
    max_title_length: profile.max_title_length,
    summary_key: profile.summary_key,
    require_summary: profile.require_summary,
    max_summary_length: profile.max_summary_length,
    draft_key: profile.draft_key,
    allow_draft: profile.allow_draft,
    require_approval_for_draft: profile.require_approval_for_draft,
    approval_key: profile.approval_key,
    tag_key: profile.tag_key,
    allowed_tags: normalize_tags(tags),
    required_tags: copy_strings(profile.required_tags),
    max_tags: profile.max_tags,
    permalink_key: profile.permalink_key,
    require_permalink: profile.require_permalink,
    permalink_prefix: profile.permalink_prefix,
    date_key: profile.date_key,
    require_date: profile.require_date,
    min_year: profile.min_year,
    max_year: profile.max_year,
  }
}

///|
pub fn profile_with_required_tags(
  profile : ContentProfile,
  tags : Array[String],
) -> ContentProfile {
  {
    required_fields: copy_strings(profile.required_fields),
    forbidden_fields: copy_strings(profile.forbidden_fields),
    title_key: profile.title_key,
    max_title_length: profile.max_title_length,
    summary_key: profile.summary_key,
    require_summary: profile.require_summary,
    max_summary_length: profile.max_summary_length,
    draft_key: profile.draft_key,
    allow_draft: profile.allow_draft,
    require_approval_for_draft: profile.require_approval_for_draft,
    approval_key: profile.approval_key,
    tag_key: profile.tag_key,
    allowed_tags: copy_strings(profile.allowed_tags),
    required_tags: normalize_tags(tags),
    max_tags: profile.max_tags,
    permalink_key: profile.permalink_key,
    require_permalink: profile.require_permalink,
    permalink_prefix: profile.permalink_prefix,
    date_key: profile.date_key,
    require_date: profile.require_date,
    min_year: profile.min_year,
    max_year: profile.max_year,
  }
}

///|
pub fn profile_with_required_field(
  profile : ContentProfile,
  key : String,
) -> ContentProfile {
  {
    required_fields: append_unique_string(profile.required_fields, key),
    forbidden_fields: copy_strings(profile.forbidden_fields),
    title_key: profile.title_key,
    max_title_length: profile.max_title_length,
    summary_key: profile.summary_key,
    require_summary: profile.require_summary,
    max_summary_length: profile.max_summary_length,
    draft_key: profile.draft_key,
    allow_draft: profile.allow_draft,
    require_approval_for_draft: profile.require_approval_for_draft,
    approval_key: profile.approval_key,
    tag_key: profile.tag_key,
    allowed_tags: copy_strings(profile.allowed_tags),
    required_tags: copy_strings(profile.required_tags),
    max_tags: profile.max_tags,
    permalink_key: profile.permalink_key,
    require_permalink: profile.require_permalink,
    permalink_prefix: profile.permalink_prefix,
    date_key: profile.date_key,
    require_date: profile.require_date,
    min_year: profile.min_year,
    max_year: profile.max_year,
  }
}

///|
pub fn profile_without_drafts(profile : ContentProfile) -> ContentProfile {
  {
    required_fields: copy_strings(profile.required_fields),
    forbidden_fields: copy_strings(profile.forbidden_fields),
    title_key: profile.title_key,
    max_title_length: profile.max_title_length,
    summary_key: profile.summary_key,
    require_summary: profile.require_summary,
    max_summary_length: profile.max_summary_length,
    draft_key: profile.draft_key,
    allow_draft: false,
    require_approval_for_draft: profile.require_approval_for_draft,
    approval_key: profile.approval_key,
    tag_key: profile.tag_key,
    allowed_tags: copy_strings(profile.allowed_tags),
    required_tags: copy_strings(profile.required_tags),
    max_tags: profile.max_tags,
    permalink_key: profile.permalink_key,
    require_permalink: profile.require_permalink,
    permalink_prefix: profile.permalink_prefix,
    date_key: profile.date_key,
    require_date: profile.require_date,
    min_year: profile.min_year,
    max_year: profile.max_year,
  }
}

///|
pub fn profile_with_permalink_prefix(
  profile : ContentProfile,
  prefix : String,
) -> ContentProfile {
  {
    required_fields: copy_strings(profile.required_fields),
    forbidden_fields: copy_strings(profile.forbidden_fields),
    title_key: profile.title_key,
    max_title_length: profile.max_title_length,
    summary_key: profile.summary_key,
    require_summary: profile.require_summary,
    max_summary_length: profile.max_summary_length,
    draft_key: profile.draft_key,
    allow_draft: profile.allow_draft,
    require_approval_for_draft: profile.require_approval_for_draft,
    approval_key: profile.approval_key,
    tag_key: profile.tag_key,
    allowed_tags: copy_strings(profile.allowed_tags),
    required_tags: copy_strings(profile.required_tags),
    max_tags: profile.max_tags,
    permalink_key: profile.permalink_key,
    require_permalink: true,
    permalink_prefix: prefix,
    date_key: profile.date_key,
    require_date: profile.require_date,
    min_year: profile.min_year,
    max_year: profile.max_year,
  }
}

///|
pub fn profile_with_date_range(
  profile : ContentProfile,
  min_year : Int,
  max_year : Int,
) -> ContentProfile {
  {
    required_fields: append_unique_string(
      profile.required_fields,
      profile.date_key,
    ),
    forbidden_fields: copy_strings(profile.forbidden_fields),
    title_key: profile.title_key,
    max_title_length: profile.max_title_length,
    summary_key: profile.summary_key,
    require_summary: profile.require_summary,
    max_summary_length: profile.max_summary_length,
    draft_key: profile.draft_key,
    allow_draft: profile.allow_draft,
    require_approval_for_draft: profile.require_approval_for_draft,
    approval_key: profile.approval_key,
    tag_key: profile.tag_key,
    allowed_tags: copy_strings(profile.allowed_tags),
    required_tags: copy_strings(profile.required_tags),
    max_tags: profile.max_tags,
    permalink_key: profile.permalink_key,
    require_permalink: profile.require_permalink,
    permalink_prefix: profile.permalink_prefix,
    date_key: profile.date_key,
    require_date: true,
    min_year,
    max_year,
  }
}

///|
pub fn evaluate_profile(
  doc : Document,
  profile : ContentProfile,
) -> ProfileReport {
  let checked : Array[String] = []
  let findings : Array[ProfileFinding] = []
  check_required_fields(doc, profile, checked, findings)
  check_forbidden_fields(doc, profile, checked, findings)
  check_title_profile(doc, profile, checked, findings)
  check_summary_profile(doc, profile, checked, findings)
  check_draft_profile(doc, profile, checked, findings)
  check_tag_profile(doc, profile, checked, findings)
  check_permalink_profile(doc, profile, checked, findings)
  check_date_profile(doc, profile, checked, findings)
  let diagnostics = profile_findings_to_diagnostics(findings, doc.start_line)
  let suggestions = profile_suggestions(findings)
  let score = profile_score(findings, checked.length())
  {
    ok: count_error_findings(findings) == 0,
    score,
    checked,
    findings,
    diagnostics,
    suggestions,
  }
}

///|
pub fn profile_ok(doc : Document, profile : ContentProfile) -> Bool {
  evaluate_profile(doc, profile).ok
}

///|
pub fn profile_findings_to_diagnostics(
  findings : Array[ProfileFinding],
  line : Int,
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for finding in findings {
    diagnostics.push(Diagnostic::{
      severity: finding.severity,
      line,
      code: profile_kind_code(finding.kind),
      message: finding.message,
    })
  }
  diagnostics
}

///|
pub fn profile_kind_code(kind : ProfileFindingKind) -> String {
  match kind {
    MissingRequired => "profile-missing-required"
    ForbiddenField => "profile-forbidden-field"
    BlankText => "profile-blank-text"
    TextTooLong => "profile-text-too-long"
    TagNotAllowed => "profile-tag-not-allowed"
    TagRequired => "profile-tag-required"
    TooManyTags => "profile-too-many-tags"
    DraftBlocked => "profile-draft-blocked"
    DraftNeedsApproval => "profile-draft-needs-review"
    PermalinkMissing => "profile-permalink-missing"
    PermalinkPrefixMismatch => "profile-permalink-prefix"
    DateMissing => "profile-date-missing"
    DateInvalid => "profile-date-invalid"
    DateOutOfRange => "profile-date-out-of-range"
  }
}

///|
pub fn profile_suggestions(findings : Array[ProfileFinding]) -> Array[String] {
  let suggestions : Array[String] = []
  for finding in findings {
    match finding.kind {
      MissingRequired =>
        push_unique_in_place(
          suggestions,
          "Add metadata field `" + finding.key + "`.",
        )
      ForbiddenField =>
        push_unique_in_place(
          suggestions,
          "Remove unsupported metadata field `" + finding.key + "`.",
        )
      BlankText =>
        push_unique_in_place(
          suggestions,
          "Fill a non-empty value for `" + finding.key + "`.",
        )
      TextTooLong =>
        push_unique_in_place(
          suggestions,
          "Shorten `" + finding.key + "` to fit the configured limit.",
        )
      TagNotAllowed =>
        push_unique_in_place(
          suggestions,
          "Replace tag `" + finding.value + "` with an allowed taxonomy tag.",
        )
      TagRequired =>
        push_unique_in_place(
          suggestions,
          "Add required tag `" + finding.value + "`.",
        )
      TooManyTags =>
        push_unique_in_place(suggestions, "Reduce the number of tags.")
      DraftBlocked =>
        push_unique_in_place(
          suggestions, "Publish-ready content must set draft to false.",
        )
      DraftNeedsApproval =>
        push_unique_in_place(
          suggestions, "Mark draft content as approved before release.",
        )
      PermalinkMissing =>
        push_unique_in_place(suggestions, "Add a permalink field.")
      PermalinkPrefixMismatch =>
        push_unique_in_place(
          suggestions, "Move permalink under the configured prefix.",
        )
      DateMissing =>
        push_unique_in_place(suggestions, "Add a YYYY-MM-DD date field.")
      DateInvalid =>
        push_unique_in_place(suggestions, "Use YYYY-MM-DD for the date field.")
      DateOutOfRange =>
        push_unique_in_place(
          suggestions, "Move the date into the configured publication range.",
        )
    }
  }
  suggestions
}

///|
pub fn profile_score(
  findings : Array[ProfileFinding],
  checked_count : Int,
) -> Int {
  let mut score = 100
  for finding in findings {
    match finding.severity {
      Error => score = score - 18
      Warning => score = score - 6
    }
  }
  if checked_count == 0 {
    score = score - 10
  }
  clamp_int(score, 0, 100)
}

///|
pub fn profile_contract_rules(profile : ContentProfile) -> Array[FieldRule] {
  let rules : Array[FieldRule] = []
  for key in profile.required_fields {
    rules.push({ key, kind: Any, required: true })
  }
  if profile.max_title_length > 0 {
    push_field_rule_unique(rules, {
      key: profile.title_key,
      kind: TextType,
      required: true,
    })
  }
  if profile.require_summary {
    push_field_rule_unique(rules, {
      key: profile.summary_key,
      kind: TextType,
      required: true,
    })
  }
  if profile.max_tags > 0 ||
    profile.allowed_tags.length() > 0 ||
    profile.required_tags.length() > 0 {
    push_field_rule_unique(rules, {
      key: profile.tag_key,
      kind: ListType,
      required: false,
    })
  }
  if profile.require_date {
    push_field_rule_unique(rules, {
      key: profile.date_key,
      kind: TextType,
      required: true,
    })
  }
  if profile.require_permalink {
    push_field_rule_unique(rules, {
      key: profile.permalink_key,
      kind: TextType,
      required: true,
    })
  }
  rules
}

///|
pub fn count_error_findings(findings : Array[ProfileFinding]) -> Int {
  let mut count = 0
  for finding in findings {
    if finding.severity == Error {
      count = count + 1
    }
  }
  count
}

///|
pub fn count_warning_findings(findings : Array[ProfileFinding]) -> Int {
  let mut count = 0
  for finding in findings {
    if finding.severity == Warning {
      count = count + 1
    }
  }
  count
}

///|
pub fn diagnostics_have_errors(diagnostics : Array[Diagnostic]) -> Bool {
  diagnostics.any(diagnostic => diagnostic.severity == Error)
}

///|
pub fn count_error_diagnostics(diagnostics : Array[Diagnostic]) -> Int {
  let mut count = 0
  for diagnostic in diagnostics {
    if diagnostic.severity == Error {
      count = count + 1
    }
  }
  count
}

///|
pub fn count_warning_diagnostics(diagnostics : Array[Diagnostic]) -> Int {
  let mut count = 0
  for diagnostic in diagnostics {
    if diagnostic.severity == Warning {
      count = count + 1
    }
  }
  count
}

///|
pub fn normalize_tag(tag : String) -> String {
  slugify(tag.trim().to_owned())
}

///|
pub fn normalize_tags(tags : Array[String]) -> Array[String] {
  let values : Array[String] = []
  for tag in tags {
    let normalized = normalize_tag(tag)
    if !normalized.is_empty() && !contains_string(values, normalized) {
      values.push(normalized)
    }
  }
  values
}

///|
pub fn contains_string(values : Array[String], value : String) -> Bool {
  values.any(item => item == value)
}

///|
pub fn copy_strings(values : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for value in values {
    result.push(value)
  }
  result
}

///|
pub fn append_unique_string(
  values : Array[String],
  value : String,
) -> Array[String] {
  let result = copy_strings(values)
  if !contains_string(result, value) {
    result.push(value)
  }
  result
}

///|
pub fn difference_strings(
  left : Array[String],
  right : Array[String],
) -> Array[String] {
  let result : Array[String] = []
  for value in left {
    if !contains_string(right, value) {
      result.push(value)
    }
  }
  result
}

///|
pub fn intersect_strings(
  left : Array[String],
  right : Array[String],
) -> Array[String] {
  let result : Array[String] = []
  for value in left {
    if contains_string(right, value) && !contains_string(result, value) {
      result.push(value)
    }
  }
  result
}

///|
pub fn dedupe_strings(values : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for value in values {
    if !contains_string(result, value) {
      result.push(value)
    }
  }
  result
}

///|
pub fn sorted_strings(values : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for value in values {
    insert_sorted_string(result, value)
  }
  result
}

///|
pub fn join_backticked(values : Array[String]) -> String {
  let parts : Array[String] = []
  for value in values {
    parts.push("`" + value + "`")
  }
  parts.join(", ")
}

///|
fn check_required_fields(
  doc : Document,
  profile : ContentProfile,
  checked : Array[String],
  findings : Array[ProfileFinding],
) -> Unit {
  for key in profile.required_fields {
    checked.push("required:" + key)
    if !doc.fields.contains(key) {
      findings.push(
        make_finding(
          MissingRequired,
          key,
          "",
          "required field `" + key + "` is missing",
          Error,
        ),
      )
    }
  }
}

///|
fn check_forbidden_fields(
  doc : Document,
  profile : ContentProfile,
  checked : Array[String],
  findings : Array[ProfileFinding],
) -> Unit {
  for key in profile.forbidden_fields {
    checked.push("forbidden:" + key)
    if doc.fields.contains(key) {
      findings.push(
        make_finding(
          ForbiddenField,
          key,
          "",
          "field `" + key + "` is forbidden by profile",
          Error,
        ),
      )
    }
  }
}

///|
fn check_title_profile(
  doc : Document,
  profile : ContentProfile,
  checked : Array[String],
  findings : Array[ProfileFinding],
) -> Unit {
  checked.push("title:" + profile.title_key)
  match get_text(doc, profile.title_key) {
    None =>
      if contains_string(profile.required_fields, profile.title_key) {
        ()
      } else {
        findings.push(
          make_finding(
            MissingRequired,
            profile.title_key,
            "",
            "title field `" + profile.title_key + "` is missing",
            Error,
          ),
        )
      }
    Some(title) => {
      let cleaned = title.trim().to_owned()
      if cleaned.is_empty() {
        findings.push(
          make_finding(
            BlankText,
            profile.title_key,
            "",
            "title field `" + profile.title_key + "` is blank",
            Error,
          ),
        )
      } else if profile.max_title_length > 0 &&
        cleaned.length() > profile.max_title_length {
        findings.push(
          make_finding(
            TextTooLong,
            profile.title_key,
            cleaned,
            "title is longer than " + profile.max_title_length.to_string(),
            Warning,
          ),
        )
      }
    }
  }
}

///|
fn check_summary_profile(
  doc : Document,
  profile : ContentProfile,
  checked : Array[String],
  findings : Array[ProfileFinding],
) -> Unit {
  checked.push("summary:" + profile.summary_key)
  match get_text(doc, profile.summary_key) {
    None =>
      if profile.require_summary {
        findings.push(
          make_finding(
            MissingRequired,
            profile.summary_key,
            "",
            "summary field `" + profile.summary_key + "` is required",
            Error,
          ),
        )
      }
    Some(summary) => {
      let cleaned = summary.trim().to_owned()
      if cleaned.is_empty() {
        findings.push(
          make_finding(
            BlankText,
            profile.summary_key,
            "",
            "summary field `" + profile.summary_key + "` is blank",
            Error,
          ),
        )
      } else if profile.max_summary_length > 0 &&
        cleaned.length() > profile.max_summary_length {
        findings.push(
          make_finding(
            TextTooLong,
            profile.summary_key,
            cleaned,
            "summary is longer than " + profile.max_summary_length.to_string(),
            Warning,
          ),
        )
      }
    }
  }
}

///|
fn check_draft_profile(
  doc : Document,
  profile : ContentProfile,
  checked : Array[String],
  findings : Array[ProfileFinding],
) -> Unit {
  checked.push("draft:" + profile.draft_key)
  let draft = get_bool(doc, profile.draft_key).unwrap_or(false)
  if draft && !profile.allow_draft {
    findings.push(
      make_finding(
        DraftBlocked,
        profile.draft_key,
        "true",
        "draft content is blocked by profile",
        Error,
      ),
    )
  }
  if draft && profile.require_approval_for_draft {
    let approved = get_bool(doc, profile.approval_key).unwrap_or(false)
    if !approved {
      findings.push(
        make_finding(
          DraftNeedsApproval,
          profile.approval_key,
          "false",
          "draft content requires `" + profile.approval_key + "`",
          Warning,
        ),
      )
    }
  }
}

///|
fn check_tag_profile(
  doc : Document,
  profile : ContentProfile,
  checked : Array[String],
  findings : Array[ProfileFinding],
) -> Unit {
  checked.push("tags:" + profile.tag_key)
  let tags = normalized_doc_tags(doc, profile.tag_key)
  if profile.max_tags > 0 && tags.length() > profile.max_tags {
    findings.push(
      make_finding(
        TooManyTags,
        profile.tag_key,
        tags.length().to_string(),
        "too many tags: " + tags.length().to_string(),
        Warning,
      ),
    )
  }
  if profile.allowed_tags.length() > 0 {
    for tag in tags {
      if !contains_string(profile.allowed_tags, tag) {
        findings.push(
          make_finding(
            TagNotAllowed,
            profile.tag_key,
            tag,
            "tag `" + tag + "` is not in the allowed taxonomy",
            Error,
          ),
        )
      }
    }
  }
  for required in profile.required_tags {
    if !contains_string(tags, required) {
      findings.push(
        make_finding(
          TagRequired,
          profile.tag_key,
          required,
          "required tag `" + required + "` is missing",
          Error,
        ),
      )
    }
  }
}

///|
fn check_permalink_profile(
  doc : Document,
  profile : ContentProfile,
  checked : Array[String],
  findings : Array[ProfileFinding],
) -> Unit {
  checked.push("permalink:" + profile.permalink_key)
  match get_text(doc, profile.permalink_key) {
    None =>
      if profile.require_permalink {
        findings.push(
          make_finding(
            PermalinkMissing,
            profile.permalink_key,
            "",
            "permalink field `" + profile.permalink_key + "` is required",
            Error,
          ),
        )
      }
    Some(permalink) => {
      let cleaned = permalink.trim().to_owned()
      if cleaned.is_empty() {
        findings.push(
          make_finding(
            PermalinkMissing,
            profile.permalink_key,
            "",
            "permalink field `" + profile.permalink_key + "` is blank",
            Error,
          ),
        )
      } else if !profile.permalink_prefix.is_empty() &&
        !cleaned.has_prefix(profile.permalink_prefix) {
        findings.push(
          make_finding(
            PermalinkPrefixMismatch,
            profile.permalink_key,
            cleaned,
            "permalink must start with `" + profile.permalink_prefix + "`",
            Error,
          ),
        )
      }
    }
  }
}

///|
fn check_date_profile(
  doc : Document,
  profile : ContentProfile,
  checked : Array[String],
  findings : Array[ProfileFinding],
) -> Unit {
  checked.push("date:" + profile.date_key)
  match get_text(doc, profile.date_key) {
    None =>
      if profile.require_date {
        findings.push(
          make_finding(
            DateMissing,
            profile.date_key,
            "",
            "date field `" + profile.date_key + "` is required",
            Error,
          ),
        )
      }
    Some(date) =>
      if !is_iso_date(date) {
        findings.push(
          make_finding(
            DateInvalid,
            profile.date_key,
            date,
            "date must use YYYY-MM-DD",
            Error,
          ),
        )
      } else {
        let year = parse_year(date)
        if year < profile.min_year || year > profile.max_year {
          findings.push(
            make_finding(
              DateOutOfRange,
              profile.date_key,
              date,
              "date year is outside configured range",
              Error,
            ),
          )
        }
      }
  }
}

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

///|
fn make_finding(
  kind : ProfileFindingKind,
  key : String,
  value : String,
  message : String,
  severity : Severity,
) -> ProfileFinding {
  { kind, key, value, message, severity }
}

///|
fn push_unique_in_place(values : Array[String], value : String) -> Unit {
  if !contains_string(values, value) {
    values.push(value)
  }
}

///|
fn push_field_rule_unique(rules : Array[FieldRule], rule : FieldRule) -> Unit {
  let mut found = false
  for existing in rules {
    if existing.key == rule.key {
      found = true
    }
  }
  if !found {
    rules.push(rule)
  }
}

///|
fn insert_sorted_string(values : Array[String], value : String) -> Unit {
  if values.length() == 0 {
    values.push(value)
    return
  }
  for i in 0.. Bool {
  let limit = if left.length() < right.length() {
    left.length()
  } else {
    right.length()
  }
  for i in 0.. right_code {
      return false
    }
  }
  left.length() <= right.length()
}

///|
fn is_iso_date(value : String) -> Bool {
  if value.length() != 10 {
    return false
  }
  if value.get_char(4).unwrap() != '-' || value.get_char(7).unwrap() != '-' {
    return false
  }
  digits_only(value.sub(start=0, end=4).to_owned()) &&
  digits_only(value.sub(start=5, end=7).to_owned()) &&
  digits_only(value.sub(start=8, end=10).to_owned()) &&
  valid_month_day(value)
}

///|
fn valid_month_day(value : String) -> Bool {
  let month = parse_two_digits(value, 5)
  let day = parse_two_digits(value, 8)
  month >= 1 && month <= 12 && day >= 1 && day <= days_in_month(month)
}

///|
fn days_in_month(month : Int) -> Int {
  match month {
    2 => 29
    4 => 30
    6 => 30
    9 => 30
    11 => 30
    _ => 31
  }
}

///|
fn parse_year(value : String) -> Int {
  parse_fixed_digits(value, 0, 4)
}

///|
fn parse_two_digits(value : String, start : Int) -> Int {
  parse_fixed_digits(value, start, start + 2)
}

///|
fn parse_fixed_digits(value : String, start : Int, end : Int) -> Int {
  let mut result = 0
  for i in start.. Bool {
  if value.is_empty() {
    return false
  }
  value.all(ch => {
    let code = ch.to_int()
    code >= 48 && code <= 57
  })
}

///|
fn clamp_int(value : Int, low : Int, high : Int) -> Int {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}