///|
/// Schema lint 的问题级别。Error 表示 Schema 元数据缺少关键安全约束,
/// Warning 表示协议形状可工作,但会降低静态分析能力或扩大误用风险。
pub(all) enum LintSeverity {
  LintWarning
  LintError
} derive(Eq, Debug, ToJson)

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

///|
pub extend LintSeverity with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend LintSeverity with ToJson::{to_json}

///|
pub fn LintSeverity::name(self : LintSeverity) -> String {
  match self {
    LintWarning => "warning"
    LintError => "error"
  }
}

///|
/// Schema linter 产生的稳定、机器可读问题。
pub(all) struct LintIssue {
  code : String
  severity : LintSeverity
  path : String
  message : String
} derive(Eq, Debug, ToJson)

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

///|
pub extend LintIssue with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend LintIssue with ToJson::{to_json}

///|
pub fn LintIssue::render(self : LintIssue) -> String {
  "[\{self.severity.name()}] \{self.code} at \{self.path}: \{self.message}"
}

///|
fn lint_string_starts_with(value : String, prefix : String) -> Bool {
  if value.length() < prefix.length() {
    return false
  }
  for index in 0.. Bool {
  for constraint in node.constraints {
    if lint_string_starts_with(constraint, prefix) {
      return true
    }
  }
  false
}

///|
fn lint_issue(
  issues : Array[LintIssue],
  code : String,
  severity : LintSeverity,
  path : String,
  message : String,
) -> Unit {
  issues.push({ code, severity, path, message, })
}

///|
fn child_path(parent : String, child : SchemaNode, index : Int) -> String {
  let segment = if child.name == "" { "#\{index}" } else { child.name }
  if parent == "" {
    segment
  } else {
    parent + "." + segment
  }
}

///|
fn lint_duplicate_child_names(
  node : SchemaNode,
  path : String,
  issues : Array[LintIssue],
) -> Unit {
  for left = 0; left < node.children.length(); left = left + 1 {
    let name = node.children[left].name
    if name != "" {
      for right = left + 1; right < node.children.length(); right = right + 1 {
        if node.children[right].name == name {
          lint_issue(
            issues,
            "duplicate-child-name",
            LintWarning,
            path,
            "multiple child fields share the name '" +
            name +
            "', which makes traces and generated tooling ambiguous",
          )
          break
        }
      }
    }
  }
}

///|
fn lint_node(
  node : SchemaNode,
  path : String,
  issues : Array[LintIssue],
) -> Unit {
  lint_duplicate_child_names(node, path, issues)
  match node.kind {
    "length-prefixed" =>
      if !schema_has_constraint(node, "max_length=") {
        lint_issue(
          issues,
          "missing-max-length",
          LintError,
          path,
          "length-prefixed fields should declare max_length so tooling can surface a declared local allocation bound",
        )
      }
    "count-prefixed-array" =>
      if !schema_has_constraint(node, "max_count=") {
        lint_issue(
          issues,
          "missing-max-count",
          LintError,
          path,
          "count-prefixed arrays should declare max_count so tooling can surface a declared local collection bound",
        )
      }
    "bounded" =>
      if !schema_has_constraint(node, "length=") {
        lint_issue(
          issues,
          "missing-bounded-length",
          LintError,
          path,
          "bounded nodes must expose their fixed length in Schema metadata",
        )
      }
    "array" =>
      if !schema_has_constraint(node, "count=") {
        lint_issue(
          issues,
          "missing-repeat-count",
          LintError,
          path,
          "fixed arrays must expose their repeat count in Schema metadata",
        )
      }
    "tagged" =>
      if schema_has_constraint(node, "variants=dynamic") {
        lint_issue(
          issues,
          "dynamic-tagged-variants",
          LintWarning,
          path,
          "tagged variants are selected dynamically, so static tools cannot enumerate branch schemas",
        )
      }
    "until-eof-array" =>
      lint_issue(
        issues,
        "until-eof-region",
        LintWarning,
        path,
        "until_eof depends on the enclosing region boundary; prefer an explicit bounded region when the protocol provides one",
      )
    "remaining-bytes" | "remaining-view" =>
      lint_issue(
        issues,
        "remaining-region",
        LintWarning,
        path,
        "remaining_* consumes the entire enclosing region; keep it inside a protocol-defined bounded region",
      )
    _ => ()
  }
  for index, child in node.children {
    lint_node(child, child_path(path, child, index), issues)
  }
}

///|
/// 对任意 SchemaNode 执行确定性的静态检查。
pub fn lint_schema(schema : SchemaNode) -> Array[LintIssue] {
  let issues : Array[LintIssue] = []
  let root = if schema.name == "" { schema.kind } else { schema.name }
  lint_node(schema, root, issues)
  issues
}

///|
/// 直接检查 Codec 自带的 Schema 元数据。
pub fn[T] Codec::lint(self : Codec[T]) -> Array[LintIssue] {
  lint_schema(self.schema)
}