///|
/// Message and term namespaces are distinct. Term names omit the leading '-'.
pub(all) enum SymbolKind {
  MessageSymbol
  TermSymbol
} derive(Eq)

///|
pub(all) struct Definition {
  kind : SymbolKind
  name : String
  node : Node
  attributes : Array[String]
  has_value : Bool
}

///|
/// Only message/term references are resolution targets; variables and functions
/// are external runtime inputs, and are deliberately excluded.
pub(all) struct Reference {
  kind : SymbolKind
  name : String
  attribute : String?
  span : Span
} derive(Eq)

///|
pub(all) struct DuplicateDefinition {
  kind : SymbolKind
  name : String
  first_span : Span
  duplicate_span : Span
} derive(Eq)

///|
pub(all) enum MissingReferenceReason {
  UnknownDefinition
  UnknownAttribute
  MissingValue
} derive(Eq)

///|
pub(all) struct MissingReference {
  reference : Reference
  reason : MissingReferenceReason
} derive(Eq)

///|
pub(all) enum CatalogError {
  Traversal(TreeError)
  ExpectedResource
  InvalidIdentifier(String)
  MalformedNode(String, Span)
  JunkPresent(Span)
  MissingDefinition(SymbolKind, String)
  AmbiguousDefinition(SymbolKind, String)
  RenameConflict(SymbolKind, String)
} derive(Eq)

///|
/// Immutable index snapshot. Returned arrays are copies of index storage.
/// Node payloads are shared, so callers must not mutate AST arrays after indexing.
pub struct Catalog {
  entries : Array[Definition]
  uses : Array[Reference]
  repeated : Array[DuplicateDefinition]
  junk : Array[Span]
  messages : Map[String, Array[Int32]]
  terms : Map[String, Array[Int32]]
}

///|
/// Fluent identifier syntax, without the term sigil. ASCII letters start an id;
/// subsequent characters may also be digits, '_' or '-'.
pub fn valid_symbol_id(name : String) -> Bool {
  let chars = name.to_array()
  if chars.is_empty() {
    return false
  }
  fn letter(c : Char) -> Bool {
    (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
  }
  if !letter(chars[0]) {
    return false
  }
  for c in chars {
    if !letter(c) && !(c >= '0' && c <= '9') && c != '_' && c != '-' {
      return false
    }
  }
  true
}

///|
fn catalog_identifier(node : Node) -> Result[String, CatalogError] {
  match node.kind {
    Identifier(name) =>
      if valid_symbol_id(name) {
        Ok(name)
      } else {
        Err(InvalidIdentifier(name))
      }
    _ => Err(MalformedNode("expected Identifier", node.span))
  }
}

///|
fn catalog_attribute(node : Node?) -> Result[String?, CatalogError] {
  match node {
    None => Ok(None)
    Some(n) =>
      match catalog_identifier(n) {
        Ok(name) => Ok(Some(name))
        Err(error) => Err(error)
      }
  }
}

///|
/// Build a source-ordered index, preserving all duplicate definitions rather
/// than silently choosing a winner. Junk is recorded, not discarded. Definitions
/// must be Resource children; nested definitions/resources are rejected.
pub fn Catalog::build(
  resource : Node,
  max_depth~ : Int32 = 256,
  max_nodes~ : Int32 = 100000,
) -> Result[Catalog, CatalogError] {
  guard resource.kind is Resource(_) else { return Err(ExpectedResource) }
  let visits = match resource.walk(max_depth~, max_nodes~) {
    Ok(v) => v
    Err(e) => return Err(Traversal(e))
  }
  let entries : Array[Definition] = []
  let uses : Array[Reference] = []
  let repeated : Array[DuplicateDefinition] = []
  let junk : Array[Span] = []
  let messages : Map[String, Array[Int32]] = Map([])
  let terms : Map[String, Array[Int32]] = Map([])
  for visit in visits {
    let node = visit.node
    let definition = match node.kind {
      Message(id, value, attrs, _) =>
        Some((MessageSymbol, id, attrs, value is Some(_)))
      Term(id, _, attrs, _) => Some((TermSymbol, id, attrs, true))
      _ => None
    }
    if definition is Some((kind, id, attrs, has_value)) {
      if visit.depth != 1 {
        return Err(MalformedNode("definition outside Resource body", node.span))
      }
      let name = match catalog_identifier(id) {
        Ok(name) => name
        Err(e) => return Err(e)
      }
      let attributes : Array[String] = []
      for attr in attrs {
        guard attr.kind is Attribute(attr_id, _) else {
          return Err(MalformedNode("expected Attribute", attr.span))
        }
        match catalog_identifier(attr_id) {
          Ok(name) => attributes.push(name)
          Err(e) => return Err(e)
        }
      }
      let index = match kind {
        MessageSymbol => messages
        TermSymbol => terms
      }
      let positions = match index.get(name) {
        Some(positions) => {
          repeated.push({
            kind,
            name,
            first_span: entries[positions[0]].node.span,
            duplicate_span: node.span,
          })
          positions
        }
        None => {
          let positions : Array[Int32] = []
          index[name] = positions
          positions
        }
      }
      positions.push(entries.length())
      entries.push({ kind, name, node, attributes, has_value, })
    }
    match node.kind {
      MessageReference(id, attr) | TermReference(id, attr, _) => {
        let kind = match node.kind {
          TermReference(_, _, _) => TermSymbol
          _ => MessageSymbol
        }
        let name = match catalog_identifier(id) {
          Ok(name) => name
          Err(e) => return Err(e)
        }
        let attribute = match catalog_attribute(attr) {
          Ok(a) => a
          Err(e) => return Err(e)
        }
        uses.push({ kind, name, attribute, span: node.span, })
      }
      Junk(_, _) => junk.push(node.span)
      Resource(_) =>
        if visit.depth != 0 {
          return Err(MalformedNode("nested Resource", node.span))
        }
      _ => ()
    }
  }
  Ok({ entries, uses, repeated, junk, messages, terms, })
}

///|
pub fn Catalog::definitions(self : Catalog) -> Array[Definition] {
  // Copy attribute arrays too; node payloads remain shared as documented.
  self.entries.map(d => { ..d, attributes: d.attributes.map(a => a), })
}

///|
pub fn Catalog::references(self : Catalog) -> Array[Reference] {
  self.uses.map(r => r)
}

///|
pub fn Catalog::duplicates(self : Catalog) -> Array[DuplicateDefinition] {
  self.repeated.map(d => d)
}

///|
pub fn Catalog::junk_spans(self : Catalog) -> Array[Span] {
  self.junk.map(s => s)
}

///|
/// Exact namespace lookup; returns every match in source order, including duplicates.
pub fn Catalog::find(
  self : Catalog,
  kind : SymbolKind,
  name : String,
) -> Array[Definition] {
  let index = match kind {
    MessageSymbol => self.messages
    TermSymbol => self.terms
  }
  match index.get(name) {
    Some(positions) =>
      positions.map(i => {
        let d = self.entries[i]
        { ..d, attributes: d.attributes.map(a => a), }
      })
    None => []
  }
}

///|
/// Resolve against this resource only. Attribute access requires that attribute;
/// bare access requires a value. With duplicates, any matching definition suffices;
/// ambiguity is reported separately by duplicates(). Junk may hide further uses.
pub fn Catalog::missing_references(self : Catalog) -> Array[MissingReference] {
  let result : Array[MissingReference] = []
  for reference in self.uses {
    let targets = self.find(reference.kind, reference.name)
    if targets.is_empty() {
      result.push({ reference, reason: UnknownDefinition, })
    } else {
      let mut found = false
      for target in targets {
        match reference.attribute {
          Some(attr) => if target.attributes.contains(attr) { found = true }
          None => if target.has_value { found = true }
        }
      }
      if !found {
        result.push({
          reference,
          reason: match reference.attribute {
            Some(_) => UnknownAttribute
            None => MissingValue
          },
        })
      }
    }
  }
  result
}

///|
fn catalog_renamed_id(id : Node, old_name : String, new_name : String) -> Node {
  match id.kind {
    Identifier(name) if name == old_name =>
      { kind: Identifier(new_name), span: id.span, }
    _ => id
  }
}

///|
/// Transactional, syntax-aware rename of one definition and all matching
/// references, including attributes and nested term call arguments. Text, comments,
/// variables, function names and the other namespace are untouched. Conflicts,
/// invalid ids, missing/ambiguous sources, traversal limits and any Junk fail
/// before edits. An identity rename still validates input. Spans remain source
/// provenance, not recalculated offsets for a future serialization.
pub fn Node::rename_symbol(
  self : Node,
  kind : SymbolKind,
  old_name : String,
  new_name : String,
  max_depth~ : Int32 = 256,
  max_nodes~ : Int32 = 100000,
) -> Result[Node, CatalogError] {
  if !valid_symbol_id(old_name) {
    return Err(InvalidIdentifier(old_name))
  }
  if !valid_symbol_id(new_name) {
    return Err(InvalidIdentifier(new_name))
  }
  let catalog = match Catalog::build(self, max_depth~, max_nodes~) {
    Ok(c) => c
    Err(e) => return Err(e)
  }
  if !catalog.junk.is_empty() {
    return Err(JunkPresent(catalog.junk[0]))
  }
  let source = catalog.find(kind, old_name)
  if source.is_empty() {
    return Err(MissingDefinition(kind, old_name))
  }
  if source.length() != 1 {
    return Err(AmbiguousDefinition(kind, old_name))
  }
  if old_name != new_name && !catalog.find(kind, new_name).is_empty() {
    return Err(RenameConflict(kind, new_name))
  }
  self
  .transform(
    n => {
      let renamed = match n.kind {
        Message(id, value, attrs, comment) if kind == MessageSymbol =>
          Message(
            catalog_renamed_id(id, old_name, new_name),
            value,
            attrs,
            comment,
          )
        Term(id, value, attrs, comment) if kind == TermSymbol =>
          Term(
            catalog_renamed_id(id, old_name, new_name),
            value,
            attrs,
            comment,
          )
        MessageReference(id, attr) if kind == MessageSymbol =>
          MessageReference(catalog_renamed_id(id, old_name, new_name), attr)
        TermReference(id, attr, args) if kind == TermSymbol =>
          TermReference(catalog_renamed_id(id, old_name, new_name), attr, args)
        other => other
      }
      { kind: renamed, span: n.span, }
    },
    max_depth~,
    max_nodes~,
  )
  .map_err(e => Traversal(e))
}

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

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

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

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

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

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