///|
/// Options for producing stable manifest text suitable for reviews and snapshots.
/// Normalization is deliberately opt-in: it returns a new specification and never
/// changes the caller's original data.
pub(all) struct NormalizationOptions {
  sort_records : Bool
  sort_list_values : Bool
  deduplicate_list_values : Bool
  compact_whitespace : Bool
  drop_empty_values : Bool
} derive(Eq, Debug)

///|
pub(all) struct NormalizationChange {
  category : String
  subject : String
  before : String
  after : String
} derive(Eq, Debug)

///|
pub(all) struct NormalizationResult {
  spec : BoundarySpec
  changes : Array[NormalizationChange]
  source_issues : Array[TraceIssue]
} derive(Eq, Debug)

///|
pub fn default_normalization_options() -> NormalizationOptions {
  {
    sort_records: true,
    sort_list_values: true,
    deduplicate_list_values: true,
    compact_whitespace: true,
    drop_empty_values: true,
  }
}

///|
pub fn preserve_order_normalization_options() -> NormalizationOptions {
  {
    sort_records: false,
    sort_list_values: false,
    deduplicate_list_values: true,
    compact_whitespace: true,
    drop_empty_values: true,
  }
}

///|
/// Parse, normalize, and describe the changes made to a manifest. Invalid source
/// records are retained as source issues; callers should only overwrite files
/// when `result.is_lossless()` is true.
pub fn normalize_manifest(raw : StringView) -> NormalizationResult {
  normalize_manifest_with_options(raw, default_normalization_options())
}

///|
pub fn normalize_manifest_with_options(
  raw : StringView,
  options : NormalizationOptions,
) -> NormalizationResult {
  let source = parse_manifest(raw)
  normalize_spec(source, options)
}

///|
pub fn normalize_spec(
  source : BoundarySpec,
  options : NormalizationOptions,
) -> NormalizationResult {
  let spec = {
    project: normalize_text(source.project, options),
    purpose: normalize_text(source.purpose, options),
    scopes: normalize_strings(source.scopes, options),
    out_of_scope: normalize_strings(source.out_of_scope, options),
    capabilities: normalize_capabilities(source.capabilities, options),
    requirements: normalize_requirements(source.requirements, options),
    evidence: normalize_evidence(source.evidence, options),
    issues: source.issues,
  }
  {
    spec,
    changes: collect_normalization_changes(source, spec),
    source_issues: source.issues,
  }
}

///|
pub fn NormalizationResult::is_lossless(self : NormalizationResult) -> Bool {
  self.source_issues.is_empty()
}

///|
pub fn NormalizationResult::changed(self : NormalizationResult) -> Bool {
  !self.changes.is_empty()
}

///|
pub fn NormalizationResult::to_manifest(self : NormalizationResult) -> String {
  spec_to_manifest(self.spec)
}

///|
pub fn NormalizationResult::to_markdown(self : NormalizationResult) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Normalization")
  lines.push("")
  lines.push("- Project: " + printable(self.spec.project))
  lines.push("- Lossless: " + bool_label(self.is_lossless()))
  lines.push("- Changes: " + self.changes.length().to_string())
  lines.push("- Parse issues: " + self.source_issues.length().to_string())
  lines.push("")
  lines.push("## Changes")
  lines.push("")
  lines.push("| Category | Subject | Before | After |")
  lines.push("| --- | --- | --- | --- |")
  if self.changes.is_empty() {
    lines.push("| info | all | already normalized | already normalized |")
  } else {
    for change in self.changes {
      lines.push(
        "| " +
        markdown_cell(change.category) +
        " | " +
        markdown_cell(change.subject) +
        " | " +
        markdown_cell(change.before) +
        " | " +
        markdown_cell(change.after) +
        " |",
      )
    }
  }
  if !self.source_issues.is_empty() {
    lines.push("")
    lines.push("## Source Issues")
    lines.push("")
    for issue in self.source_issues {
      lines.push(
        "- line " +
        issue.line.to_string() +
        ": " +
        issue.kind.label() +
        " - " +
        issue.message,
      )
    }
  }
  lines.join("\n")
}

///|
/// Render valid manifest records in canonical record order. This is useful for
/// snapshot tests and generated review artifacts, not as a replacement for a
/// parser error message.
pub fn spec_to_manifest(spec : BoundarySpec) -> String {
  let lines : Array[String] = []
  if !spec.project.trim().is_empty() {
    lines.push("project: " + spec.project)
  }
  if !spec.purpose.trim().is_empty() {
    lines.push("purpose: " + spec.purpose)
  }
  for scope in spec.scopes {
    lines.push("scope: " + scope)
  }
  for non_goal in spec.out_of_scope {
    lines.push("out-of-scope: " + non_goal)
  }
  for capability in spec.capabilities {
    lines.push(capability_to_manifest_line(capability))
  }
  for requirement in spec.requirements {
    lines.push(requirement_to_manifest_line(requirement))
  }
  for item in spec.evidence {
    lines.push(evidence_to_manifest_line(item))
  }
  lines.join("\n")
}

///|
pub fn capability_to_manifest_line(capability : Capability) -> String {
  "capability: " +
  capability.id +
  " | " +
  capability.title +
  " | " +
  capability.inputs.join(", ") +
  " | " +
  capability.outputs.join(", ") +
  " | " +
  capability.constraints.join(", ")
}

///|
pub fn requirement_to_manifest_line(requirement : Requirement) -> String {
  "requirement: " +
  requirement.id +
  " | " +
  requirement.level.label() +
  " | " +
  requirement.text +
  " | " +
  requirement.capabilities.join(", ")
}

///|
pub fn evidence_to_manifest_line(item : Evidence) -> String {
  "evidence: " +
  item.id +
  " | " +
  item.kind.label() +
  " | " +
  item.target +
  " | " +
  item.status.label() +
  " | " +
  item.requirements.join(", ") +
  " | " +
  item.note
}

///|
fn normalize_capabilities(
  items : Array[Capability],
  options : NormalizationOptions,
) -> Array[Capability] {
  let result : Array[Capability] = []
  for item in items {
    result.push({
      id: normalize_text(item.id, options),
      title: normalize_text(item.title, options),
      inputs: normalize_strings(item.inputs, options),
      outputs: normalize_strings(item.outputs, options),
      constraints: normalize_strings(item.constraints, options),
    })
  }
  if options.sort_records {
    sort_capabilities_by_id(result)
  } else {
    result
  }
}

///|
fn normalize_requirements(
  items : Array[Requirement],
  options : NormalizationOptions,
) -> Array[Requirement] {
  let result : Array[Requirement] = []
  for item in items {
    result.push({
      id: normalize_text(item.id, options),
      level: item.level,
      text: normalize_text(item.text, options),
      capabilities: normalize_strings(item.capabilities, options),
    })
  }
  if options.sort_records {
    sort_requirements_by_id(result)
  } else {
    result
  }
}

///|
fn normalize_evidence(
  items : Array[Evidence],
  options : NormalizationOptions,
) -> Array[Evidence] {
  let result : Array[Evidence] = []
  for item in items {
    result.push({
      id: normalize_text(item.id, options),
      kind: item.kind,
      target: normalize_text(item.target, options),
      status: item.status,
      requirements: normalize_strings(item.requirements, options),
      note: normalize_text(item.note, options),
    })
  }
  if options.sort_records {
    sort_evidence_by_id(result)
  } else {
    result
  }
}

///|
fn normalize_strings(
  values : Array[String],
  options : NormalizationOptions,
) -> Array[String] {
  let result : Array[String] = []
  for value in values {
    let normalized = normalize_text(value, options)
    let may_push = !options.drop_empty_values || !normalized.is_empty()
    let is_new = !options.deduplicate_list_values ||
      !result.contains(normalized)
    if may_push && is_new {
      result.push(normalized)
    }
  }
  if options.sort_list_values {
    sort_strings_ascending(result)
  } else {
    result
  }
}

///|
/// The manifests involved here are intentionally small. Insertion sorting avoids
/// target-specific ordering surprises while keeping canonical output stable.
pub fn sort_strings_ascending(values : Array[String]) -> Array[String] {
  let mut sorted : Array[String] = []
  for value in values {
    let next : Array[String] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && value.lexical_compare(existing) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn sort_capabilities_by_id(values : Array[Capability]) -> Array[Capability] {
  let mut sorted : Array[Capability] = []
  for value in values {
    let next : Array[Capability] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && value.id.lexical_compare(existing.id) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn sort_requirements_by_id(values : Array[Requirement]) -> Array[Requirement] {
  let mut sorted : Array[Requirement] = []
  for value in values {
    let next : Array[Requirement] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && value.id.lexical_compare(existing.id) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn sort_evidence_by_id(values : Array[Evidence]) -> Array[Evidence] {
  let mut sorted : Array[Evidence] = []
  for value in values {
    let next : Array[Evidence] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && value.id.lexical_compare(existing.id) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn normalize_text(value : String, options : NormalizationOptions) -> String {
  let trimmed = value.trim().to_owned()
  if options.compact_whitespace {
    compact_whitespace(trimmed)
  } else {
    trimmed
  }
}

///|
fn compact_whitespace(value : String) -> String {
  let mut compacted = value.replace_all(old="\t", new=" ")
  while compacted.contains("  ") {
    compacted = compacted.replace_all(old="  ", new=" ")
  }
  compacted
}

///|
fn collect_normalization_changes(
  before : BoundarySpec,
  after : BoundarySpec,
) -> Array[NormalizationChange] {
  let changes : Array[NormalizationChange] = []
  add_normalization_change(
    changes,
    "summary",
    "project",
    before.project,
    after.project,
  )
  add_normalization_change(
    changes,
    "summary",
    "purpose",
    before.purpose,
    after.purpose,
  )
  add_normalization_change(
    changes,
    "summary",
    "scope",
    before.scopes.join(" | "),
    after.scopes.join(" | "),
  )
  add_normalization_change(
    changes,
    "summary",
    "out-of-scope",
    before.out_of_scope.join(" | "),
    after.out_of_scope.join(" | "),
  )
  for item in before.capabilities {
    match after.capability(item.id) {
      Some(normalized) =>
        add_normalization_change(
          changes,
          "capability",
          item.id,
          capability_to_manifest_line(item),
          capability_to_manifest_line(normalized),
        )
      None => ()
    }
  }
  for item in before.requirements {
    match after.requirement(item.id) {
      Some(normalized) =>
        add_normalization_change(
          changes,
          "requirement",
          item.id,
          requirement_to_manifest_line(item),
          requirement_to_manifest_line(normalized),
        )
      None => ()
    }
  }
  for item in before.evidence {
    match after.evidence_item(item.id) {
      Some(normalized) =>
        add_normalization_change(
          changes,
          "evidence",
          item.id,
          evidence_to_manifest_line(item),
          evidence_to_manifest_line(normalized),
        )
      None => ()
    }
  }
  changes
}

///|
fn add_normalization_change(
  changes : Array[NormalizationChange],
  category : String,
  subject : String,
  before : String,
  after : String,
) -> Unit {
  if before != after {
    changes.push({ category, subject, before, after })
  }
}