///|
pub(all) struct RuleCatalogEntry {
  rule_id : String
  risk : RiskKind
  default_severity : Severity
  category : String
  title : String
  explanation : String
  safe_default : String
} derive(Debug, Eq)

///|
fn catalog_entry(
  rule_id : String,
  risk : RiskKind,
  severity : Severity,
  category : String,
  title : String,
  explanation : String,
  safe_default : String,
) -> RuleCatalogEntry {
  {
    rule_id,
    risk,
    default_severity: severity,
    category,
    title,
    explanation,
    safe_default,
  }
}

///|
pub fn rule_catalog() -> Array[RuleCatalogEntry] {
  [
    catalog_entry(
      "TP-C0-001",
      Notification,
      Warning,
      "C0 control",
      "Bell notification",
      "BEL can trigger audible or visual attention signals when replayed in terminals.",
      "Drop unless the output is trusted and interactive.",
    ),
    catalog_entry(
      "TP-C0-002",
      ScreenRewrite,
      Dangerous,
      "C0 control",
      "Inline screen rewrite",
      "Backspace and carriage return can visually overwrite earlier log text.",
      "Mark or drop in public artifacts.",
    ),
    catalog_entry(
      "TP-C0-003",
      UnknownControl,
      Warning,
      "C0 control",
      "NUL in text stream",
      "NUL is not meaningful in line-oriented text logs and can confuse consumers.",
      "Drop from display output and keep raw input separately.",
    ),
    catalog_entry(
      "TP-C0-099",
      UnknownControl,
      Warning,
      "C0 control",
      "Unknown non-printing control",
      "Non-printing C0 or C1 controls have terminal-specific behavior.",
      "Drop unless an explicit allow-list documents the behavior.",
    ),
    catalog_entry(
      "TP-CSI-000",
      MalformedSequence,
      Dangerous,
      "CSI",
      "Malformed CSI sequence",
      "A CSI command without a final byte is incomplete and unsafe to replay.",
      "Drop and inspect the raw source around the offset.",
    ),
    catalog_entry(
      "TP-CSI-001",
      Styling,
      Info,
      "CSI",
      "SGR styling",
      "Text style and color controls can reduce reproducibility or hide text in some themes.",
      "Keep only for trusted CI display; strip from durable artifacts.",
    ),
    catalog_entry(
      "TP-CSI-010",
      CursorMovement,
      Dangerous,
      "CSI",
      "Cursor movement",
      "Cursor movement can reorder visible output or overwrite earlier lines.",
      "Reject in public logs unless captured from a trusted progress renderer.",
    ),
    catalog_entry(
      "TP-CSI-011",
      ScreenErase,
      Dangerous,
      "CSI",
      "Screen erase",
      "Erase commands can hide previous output, and erase-scrollback variants are critical.",
      "Block erase-scrollback; sanitize all erase commands before display.",
    ),
    catalog_entry(
      "TP-CSI-012",
      ModeChange,
      Dangerous,
      "CSI",
      "Terminal mode change",
      "Mode changes can alter cursor, paste, alternate-screen, or keyboard behavior.",
      "Drop unless a narrow trusted integration requires it.",
    ),
    catalog_entry(
      "TP-CSI-013",
      TerminalQuery,
      Warning,
      "CSI",
      "Terminal query",
      "Device queries can solicit terminal responses and should not be replayed from logs.",
      "Drop from archived logs and web output.",
    ),
    catalog_entry(
      "TP-CSI-014",
      ScreenRewrite,
      Dangerous,
      "CSI",
      "Saved cursor or scrolling region",
      "Saved cursor, scroll, and region commands can make the visible log diverge from raw order.",
      "Mark or drop and prefer non-interactive capture modes.",
    ),
    catalog_entry(
      "TP-CSI-015",
      ModeChange,
      Dangerous,
      "CSI",
      "Window manipulation",
      "Window manipulation commands are unsafe in untrusted terminal transcripts.",
      "Drop and review the producing command.",
    ),
    catalog_entry(
      "TP-CSI-099",
      UnknownControl,
      Warning,
      "CSI",
      "Unknown CSI command",
      "Unknown CSI commands are not portable and may be interpreted differently by terminals.",
      "Drop until a rule documents the exact behavior.",
    ),
    catalog_entry(
      "TP-OSC-000",
      MalformedSequence,
      Dangerous,
      "OSC",
      "Malformed OSC command",
      "Malformed operating-system commands may hide payloads in terminal strings.",
      "Drop and keep the visible context for review.",
    ),
    catalog_entry(
      "TP-OSC-001",
      TitleChange,
      Dangerous,
      "OSC",
      "Title change",
      "Window or tab title updates can disguise the identity of a terminal session.",
      "Drop from copied logs and generated documentation.",
    ),
    catalog_entry(
      "TP-OSC-004",
      ModeChange,
      Dangerous,
      "OSC",
      "Palette mutation",
      "Palette changes can make later text misleading, invisible, or theme dependent.",
      "Drop and publish theme-independent text.",
    ),
    catalog_entry(
      "TP-OSC-007",
      WorkingDirectory,
      Warning,
      "OSC",
      "Working-directory metadata",
      "Directory metadata can leak local paths from personal machines or build agents.",
      "Drop unless path disclosure is intentional.",
    ),
    catalog_entry(
      "TP-OSC-008",
      Hyperlink,
      Dangerous,
      "OSC",
      "Terminal hyperlink",
      "OSC 8 can bind visible text to a hidden destination, including high-risk URI schemes.",
      "Expand to visible URLs and block dangerous schemes.",
    ),
    catalog_entry(
      "TP-OSC-009",
      Notification,
      Dangerous,
      "OSC",
      "Desktop notification",
      "Terminal notification extensions can create deceptive user-interface signals.",
      "Drop outside trusted interactive sessions.",
    ),
    catalog_entry(
      "TP-OSC-052",
      ClipboardWrite,
      Critical,
      "OSC",
      "Clipboard write",
      "OSC 52 can replace the user's clipboard when replayed by compatible terminals.",
      "Block and regenerate the log from a trusted source.",
    ),
    catalog_entry(
      "TP-OSC-099",
      UnknownControl,
      Warning,
      "OSC",
      "Unknown OSC command",
      "Unknown operating-system commands may trigger terminal-specific side effects.",
      "Drop until reviewed and documented.",
    ),
    catalog_entry(
      "TP-OSC-133",
      ModeChange,
      Warning,
      "OSC",
      "Shell integration marker",
      "Shell integration markers are metadata and should not affect archived log meaning.",
      "Drop from untrusted output.",
    ),
    catalog_entry(
      "TP-OSC-1337",
      FileTransfer,
      Critical,
      "OSC",
      "Terminal file transfer",
      "Terminal-specific extensions can embed file content or request file movement.",
      "Block and store attachments as explicit reviewed files.",
    ),
    catalog_entry(
      "TP-STR-001",
      DeviceControl,
      Critical,
      "Control string",
      "Opaque device-control string",
      "DCS, SOS, PM, and APC strings can carry device-specific opaque commands.",
      "Block unless a documented allow-list covers the exact extension.",
    ),
    catalog_entry(
      "TP-ESC-001",
      CursorMovement,
      Dangerous,
      "ESC",
      "Escape cursor command",
      "Single-byte escape cursor commands can rewrite the visible terminal surface.",
      "Drop or mark in permanent logs.",
    ),
    catalog_entry(
      "TP-ESC-002",
      ModeChange,
      Critical,
      "ESC",
      "Full terminal reset",
      "A full terminal reset changes terminal state and is unsafe to replay from artifacts.",
      "Block release artifacts containing this control.",
    ),
    catalog_entry(
      "TP-ESC-099",
      UnknownControl,
      Warning,
      "ESC",
      "Unknown escape command",
      "Unknown escape commands are not safe to replay across terminal emulators.",
      "Drop unless explicitly reviewed.",
    ),
    catalog_entry(
      "TP-PARSE-001",
      MalformedSequence,
      Dangerous,
      "Parser",
      "Invalid or truncated sequence",
      "Parser-level invalid data can indicate corrupted or evasive terminal input.",
      "Drop and inspect the raw bytes around the offset.",
    ),
    catalog_entry(
      "TP-UNI-001",
      BidirectionalText,
      Dangerous,
      "Unicode",
      "Bidirectional text control",
      "Unicode bidi controls can disguise visual order in filenames or source code.",
      "Strip and review the surrounding text.",
    ),
  ]
}

///|
pub fn rule_catalog_find(rule_id : String) -> RuleCatalogEntry? {
  for entry in rule_catalog() {
    if entry.rule_id == rule_id {
      return Some(entry)
    }
  }
  None
}

///|
pub fn rule_catalog_for_risk(risk : RiskKind) -> Array[RuleCatalogEntry] {
  let entries : Array[RuleCatalogEntry] = []
  for entry in rule_catalog() {
    if entry.risk == risk {
      entries.push(entry)
    }
  }
  entries
}

///|
pub fn rule_catalog_for_category(category : String) -> Array[RuleCatalogEntry] {
  let entries : Array[RuleCatalogEntry] = []
  for entry in rule_catalog() {
    if lower_ascii(entry.category) == lower_ascii(category) {
      entries.push(entry)
    }
  }
  entries
}

///|
pub fn finding_catalog_entry(finding : Finding) -> RuleCatalogEntry? {
  rule_catalog_find(finding.rule_id)
}

///|
pub fn rule_catalog_title(rule_id : String) -> String {
  match rule_catalog_find(rule_id) {
    Some(entry) => entry.title
    None => "Unknown TermProof rule"
  }
}

///|
pub fn rule_catalog_safe_default(rule_id : String) -> String {
  match rule_catalog_find(rule_id) {
    Some(entry) => entry.safe_default
    None => "Drop and review the terminal control before display."
  }
}

///|
pub fn rule_catalog_text() -> String {
  let out = StringBuilder::new()
  for entry in rule_catalog() {
    out.write_string(entry.rule_id)
    out.write_string(" ")
    out.write_string(severity_name(entry.default_severity))
    out.write_string(" ")
    out.write_string(risk_name(entry.risk))
    out.write_string(": ")
    out.write_string(entry.title)
    out.write_char('\n')
  }
  out.to_string()
}

///|
pub fn rule_catalog_markdown() -> String {
  let out = StringBuilder::new()
  out.write_string(
    "| Rule | Severity | Risk | Category | Title | Safe default |\n",
  )
  out.write_string("|---|---|---|---|---|---|\n")
  for entry in rule_catalog() {
    out.write_string("| ")
    out.write_string(markdown_escape(entry.rule_id))
    out.write_string(" | ")
    out.write_string(markdown_escape(severity_name(entry.default_severity)))
    out.write_string(" | ")
    out.write_string(markdown_escape(risk_name(entry.risk)))
    out.write_string(" | ")
    out.write_string(markdown_escape(entry.category))
    out.write_string(" | ")
    out.write_string(markdown_escape(entry.title))
    out.write_string(" | ")
    out.write_string(markdown_escape(entry.safe_default))
    out.write_string(" |\n")
  }
  out.to_string()
}

///|
fn write_catalog_entry_json(
  out : StringBuilder,
  entry : RuleCatalogEntry,
) -> Unit {
  out.write_string("{\"rule_id\":")
  out.write_string(json_quote(entry.rule_id))
  out.write_string(",\"severity\":")
  out.write_string(json_quote(severity_name(entry.default_severity)))
  out.write_string(",\"risk\":")
  out.write_string(json_quote(risk_name(entry.risk)))
  out.write_string(",\"category\":")
  out.write_string(json_quote(entry.category))
  out.write_string(",\"title\":")
  out.write_string(json_quote(entry.title))
  out.write_string(",\"explanation\":")
  out.write_string(json_quote(entry.explanation))
  out.write_string(",\"safe_default\":")
  out.write_string(json_quote(entry.safe_default))
  out.write_char('}')
}

///|
pub fn rule_catalog_json() -> String {
  let out = StringBuilder::new()
  out.write_string("{\"schema\":\"urn:termproof:rule-catalog-v1\",\"rules\":[")
  let mut first = true
  for entry in rule_catalog() {
    if !first {
      out.write_char(',')
    }
    first = false
    write_catalog_entry_json(out, entry)
  }
  out.write_string("]}")
  out.to_string()
}

///|
pub fn rule_catalog_count() -> Int {
  rule_catalog().length()
}