///|
/// Severity assigned to a terminal-output finding.
pub(all) enum Severity {
  Info
  Warning
  Dangerous
} derive(Eq, Debug)

///|
/// Returns a stable display label.
pub fn Severity::label(self : Severity) -> String {
  match self {
    Info => "info"
    Warning => "warning"
    Dangerous => "dangerous"
  }
}

///|
/// Category of a terminal control sequence or control character.
pub(all) enum FindingKind {
  StyleSequence
  CursorControl
  ScreenControl
  ModeChange
  TerminalQuery
  WindowControl
  TitleChange
  Hyperlink
  ClipboardWrite
  WorkingDirectory
  Notification
  ShellIntegration
  FileTransfer
  PaletteChange
  CharacterSet
  DeviceControl
  ControlCharacter
  TruncatedSequence
  OversizedSequence
  UnknownEscape
} derive(Eq, Debug)

///|
/// Returns a stable machine-friendly category name.
pub fn FindingKind::label(self : FindingKind) -> String {
  match self {
    StyleSequence => "style_sequence"
    CursorControl => "cursor_control"
    ScreenControl => "screen_control"
    ModeChange => "mode_change"
    TerminalQuery => "terminal_query"
    WindowControl => "window_control"
    TitleChange => "title_change"
    Hyperlink => "hyperlink"
    ClipboardWrite => "clipboard_write"
    WorkingDirectory => "working_directory"
    Notification => "notification"
    ShellIntegration => "shell_integration"
    FileTransfer => "file_transfer"
    PaletteChange => "palette_change"
    CharacterSet => "character_set"
    DeviceControl => "device_control"
    ControlCharacter => "control_character"
    TruncatedSequence => "truncated_sequence"
    OversizedSequence => "oversized_sequence"
    UnknownEscape => "unknown_escape"
  }
}

///|
/// One item removed from or observed in terminal output.
pub struct Finding {
  kind : FindingKind
  severity : Severity
  offset : Int
  sequence : String
  message : String
} derive(Eq, Debug)

///|
pub fn Finding::kind(self : Finding) -> FindingKind {
  self.kind
}

///|
pub fn Finding::severity(self : Finding) -> Severity {
  self.severity
}

///|
pub fn Finding::offset(self : Finding) -> Int {
  self.offset
}

///|
pub fn Finding::sequence(self : Finding) -> String {
  self.sequence
}

///|
pub fn Finding::message(self : Finding) -> String {
  self.message
}

///|
/// Returns the captured sequence with common control bytes made visible.
pub fn Finding::sequence_preview(self : Finding) -> String {
  visible_sequence(self.sequence)
}

///|
/// Controls which harmless terminal features may pass through.
pub struct Policy {
  allow_sgr : Bool
  allow_hyperlinks : Bool
  allow_newline : Bool
  allow_tab : Bool
  max_sequence_length : Int
} derive(Eq, Debug)

///|
/// Creates a strict policy suitable for untrusted logs and AI tool output.
pub fn Policy::strict() -> Policy {
  {
    allow_sgr: false,
    allow_hyperlinks: false,
    allow_newline: true,
    allow_tab: true,
    max_sequence_length: 4096,
  }
}

///|
/// Creates a policy that keeps SGR color and emphasis sequences.
pub fn Policy::styled() -> Policy {
  {
    allow_sgr: true,
    allow_hyperlinks: false,
    allow_newline: true,
    allow_tab: true,
    max_sequence_length: 4096,
  }
}

///|
/// Creates a custom policy.
pub fn Policy::new(
  allow_sgr? : Bool = false,
  allow_hyperlinks? : Bool = false,
  allow_newline? : Bool = true,
  allow_tab? : Bool = true,
  max_sequence_length? : Int = 4096,
) -> Policy {
  {
    allow_sgr,
    allow_hyperlinks,
    allow_newline,
    allow_tab,
    max_sequence_length: if max_sequence_length < 2 {
      2
    } else {
      max_sequence_length
    },
  }
}

///|
/// Result of sanitizing one string.
pub struct ScanResult {
  text : String
  findings : Array[Finding]
  removed_count : Int
} derive(Eq, Debug)

///|
pub fn ScanResult::text(self : ScanResult) -> String {
  self.text
}

///|
pub fn ScanResult::findings(self : ScanResult) -> Array[Finding] {
  self.findings.copy()
}

///|
pub fn ScanResult::removed_count(self : ScanResult) -> Int {
  self.removed_count
}

///|
pub fn ScanResult::is_safe(self : ScanResult) -> Bool {
  self.findings.all(finding => finding.severity != Dangerous)
}

///|
pub fn ScanResult::count_severity(
  self : ScanResult,
  severity : Severity,
) -> Int {
  self.findings.fold(init=0, fn(total, finding) {
    if finding.severity == severity {
      total + 1
    } else {
      total
    }
  })
}

///|
/// Counts dangerous findings.
pub fn ScanResult::dangerous_count(self : ScanResult) -> Int {
  self.count_severity(Dangerous)
}

///|
/// Counts warning findings.
pub fn ScanResult::warning_count(self : ScanResult) -> Int {
  self.count_severity(Warning)
}

///|
/// Counts informational findings.
pub fn ScanResult::info_count(self : ScanResult) -> Int {
  self.count_severity(Info)
}

///|
/// Computes a small risk score for dashboards and CI gates.
pub fn ScanResult::risk_score(self : ScanResult) -> Int {
  self.dangerous_count() * 10 + self.warning_count() * 3 + self.info_count()
}

///|
/// Returns a stable coarse risk label.
pub fn ScanResult::risk_level(self : ScanResult) -> String {
  if self.dangerous_count() > 0 {
    "high"
  } else if self.warning_count() > 0 {
    "medium"
  } else if self.info_count() > 0 {
    "low"
  } else {
    "clean"
  }
}

///|
/// Returns a stable action label for automation and CI integration.
pub fn ScanResult::recommended_action(self : ScanResult) -> String {
  self.recommended_decision().label()
}

///|
/// Renders a compact human-readable audit report.
pub fn ScanResult::audit_report(self : ScanResult) -> String {
  let mut report = "MoonAnsi Guard audit report\n"
  report = report + "risk: \{self.risk_level()} (score=\{self.risk_score()})\n"
  report = report + "action: \{self.recommended_action()}\n"
  report = report +
    "findings: dangerous=\{self.dangerous_count()}, warning=\{self.warning_count()}, info=\{self.info_count()}\n"
  report = report + "safe text: \{self.text}\n"
  if self.findings.length() == 0 {
    report = report + "details: no terminal control sequences found\n"
  } else {
    report = report + "details:\n"
    for finding in self.findings {
      report = report +
        "- \{finding.kind.label()} [\{finding.severity.label()}] offset \{finding.offset}: \{finding.message}; sequence=\{finding.sequence_preview()}\n"
    }
  }
  report
}

///|
fn visible_sequence(sequence : String) -> String {
  let mut output = ""
  for char in sequence {
    if char == '\u{1b}' {
      output = output + "\\e"
    } else if char == '\u{07}' {
      output = output + "\\a"
    } else if char == '\r' {
      output = output + "\\r"
    } else if char == '\n' {
      output = output + "\\n"
    } else if char == '\t' {
      output = output + "\\t"
    } else if char == '\b' {
      output = output + "\\b"
    } else if char.to_int() < 0x20 || char.to_int() == 0x7f {
      output = output + ""
    } else if char.to_int() >= 0x80 && char.to_int() <= 0x9f {
      output = output + ""
    } else {
      output = output + "\{char}"
    }
  }
  output
}

///|
fn append_range(
  output : Array[Char],
  chars : Array[Char],
  start : Int,
  end : Int,
) -> Unit {
  for index = start; index < end; index = index + 1 {
    output.push(chars[index])
  }
}

///|
fn sequence_text(chars : Array[Char], start : Int, end : Int) -> String {
  String::from_array(chars[start:end])
}

///|
fn add_finding(
  findings : Array[Finding],
  kind : FindingKind,
  severity : Severity,
  offset : Int,
  sequence : String,
  message : String,
) -> Unit {
  findings.push({ kind, severity, offset, sequence, message })
}

///|
fn is_csi_final(char : Char) -> Bool {
  let code = char.to_int()
  code >= 0x40 && code <= 0x7e
}

///|
fn classify_mode_change(parameters : String) -> String {
  if parameters.contains("?2004") {
    "bracketed-paste mode change can alter how later input is interpreted"
  } else if parameters.contains("?1000") ||
    parameters.contains("?1002") ||
    parameters.contains("?1003") ||
    parameters.contains("?1005") ||
    parameters.contains("?1006") ||
    parameters.contains("?1015") {
    "mouse-tracking mode change can cause the terminal to emit input reports"
  } else if parameters.contains("?1004") {
    "focus-event mode change can cause the terminal to emit focus reports"
  } else if parameters.contains("?1047") ||
    parameters.contains("?1048") ||
    parameters.contains("?1049") {
    "alternate-screen mode change can hide previously visible output"
  } else {
    "terminal mode change can affect later input or rendering behavior"
  }
}

///|
fn classify_csi(
  parameters : String,
  final_char : Char,
) -> (FindingKind, Severity, String) {
  let is_cursor = final_char == 'A' ||
    final_char == 'B' ||
    final_char == 'C' ||
    final_char == 'D' ||
    final_char == 'E' ||
    final_char == 'F' ||
    final_char == 'G' ||
    final_char == 'H' ||
    final_char == 'f' ||
    final_char == 'd' ||
    final_char == 's' ||
    final_char == 'u'
  let is_screen = final_char == 'J' ||
    final_char == 'K' ||
    final_char == 'S' ||
    final_char == 'T' ||
    final_char == 'L' ||
    final_char == 'M' ||
    final_char == '@' ||
    final_char == 'P' ||
    final_char == 'X' ||
    final_char == 'r'
  if final_char == 'm' {
    (StyleSequence, Info, "SGR style sequence")
  } else if final_char == 'h' || final_char == 'l' {
    (ModeChange, Warning, classify_mode_change(parameters))
  } else if final_char == 'n' || final_char == 'c' {
    (
      TerminalQuery,
      Warning,
      "terminal query can trigger a device response on the input channel",
    )
  } else if final_char == 't' {
    (
      WindowControl,
      Warning,
      "window operation can resize, move, minimize, or query the terminal",
    )
  } else if final_char == 'q' {
    (ModeChange, Warning, "cursor or terminal presentation mode change")
  } else if is_cursor {
    (
      CursorControl,
      Warning,
      "cursor movement can make logs visually misleading",
    )
  } else if is_screen {
    (
      ScreenControl,
      Warning,
      "screen editing can hide or rewrite visible output",
    )
  } else {
    (DeviceControl, Warning, "terminal mode or device control sequence")
  }
}

///|
fn classify_osc(
  chars : Array[Char],
  payload_start : Int,
  payload_end : Int,
) -> (FindingKind, Severity, String) {
  let mut separator = payload_start
  while separator < payload_end && chars[separator] != ';' {
    separator = separator + 1
  }
  let command = sequence_text(chars, payload_start, separator)
  if command == "52" {
    (
      ClipboardWrite,
      Dangerous,
      "OSC 52 can write attacker-controlled data to the clipboard",
    )
  } else if command == "8" {
    (
      Hyperlink,
      Warning,
      "OSC 8 can display a link target different from visible text",
    )
  } else if command == "0" || command == "1" || command == "2" {
    (TitleChange, Warning, "terminal title modification")
  } else if command == "7" {
    (
      WorkingDirectory,
      Warning,
      "working-directory metadata can disclose or spoof a local path",
    )
  } else if command == "9" || command == "777" {
    (
      Notification,
      Warning,
      "desktop notification can display attacker-controlled content",
    )
  } else if command == "133" {
    (
      ShellIntegration,
      Warning,
      "shell-integration markers can spoof prompt and command boundaries",
    )
  } else if command == "1337" {
    (
      FileTransfer,
      Dangerous,
      "terminal-specific file or inline-image transfer payload",
    )
  } else if command == "4" ||
    command == "10" ||
    command == "11" ||
    command == "12" ||
    command == "17" ||
    command == "19" {
    (
      PaletteChange,
      Warning,
      "terminal palette modification can make text misleading or invisible",
    )
  } else {
    (DeviceControl, Warning, "operating-system command sequence")
  }
}

///|
fn scan_csi(
  chars : Array[Char],
  start : Int,
  prefix_length : Int,
  policy : Policy,
  output : Array[Char],
  findings : Array[Finding],
) -> Int {
  let length = chars.length()
  let mut cursor = start + prefix_length
  let limit = start + policy.max_sequence_length
  while cursor < length && cursor < limit && !is_csi_final(chars[cursor]) {
    cursor = cursor + 1
  }
  if cursor < length && cursor < limit {
    let end = cursor + 1
    let parameters = sequence_text(chars, start + prefix_length, cursor)
    let (kind, severity, message) = classify_csi(parameters, chars[cursor])
    let sequence = sequence_text(chars, start, end)
    if kind == StyleSequence && policy.allow_sgr {
      append_range(output, chars, start, end)
    } else {
      add_finding(findings, kind, severity, start, sequence, message)
    }
    end
  } else if cursor >= limit {
    let end = if limit < length { limit } else { length }
    add_finding(
      findings,
      OversizedSequence,
      Dangerous,
      start,
      sequence_text(chars, start, end),
      "control sequence exceeded the configured length limit",
    )
    end
  } else {
    add_finding(
      findings,
      TruncatedSequence,
      Dangerous,
      start,
      sequence_text(chars, start, length),
      "unterminated CSI sequence",
    )
    length
  }
}

///|
fn scan_string_control(
  chars : Array[Char],
  start : Int,
  prefix_length : Int,
  policy : Policy,
  kind_name : String,
  output : Array[Char],
  findings : Array[Finding],
) -> Int {
  let length = chars.length()
  let payload_start = start + prefix_length
  let limit = start + policy.max_sequence_length
  let mut cursor = payload_start
  let mut found_terminator = false
  let mut terminator_length = 0
  while cursor < length && cursor < limit && !found_terminator {
    if kind_name == "OSC" && chars[cursor] == '\u{07}' {
      found_terminator = true
      terminator_length = 1
    } else if chars[cursor].to_int() == 0x9c {
      found_terminator = true
      terminator_length = 1
    } else if chars[cursor] == '\u{1b}' &&
      cursor + 1 < length &&
      chars[cursor + 1] == '\\' {
      found_terminator = true
      terminator_length = 2
    } else {
      cursor = cursor + 1
    }
  }
  if found_terminator {
    let end = cursor + terminator_length
    let (kind, severity, message) = if kind_name == "OSC" {
      classify_osc(chars, payload_start, cursor)
    } else {
      (
        DeviceControl,
        Dangerous,
        "\{kind_name} payload can trigger terminal-specific behavior",
      )
    }
    if kind == Hyperlink && policy.allow_hyperlinks {
      append_range(output, chars, start, end)
    } else {
      add_finding(
        findings,
        kind,
        severity,
        start,
        sequence_text(chars, start, end),
        message,
      )
    }
    end
  } else if cursor >= limit {
    let end = if limit < length { limit } else { length }
    add_finding(
      findings,
      OversizedSequence,
      Dangerous,
      start,
      sequence_text(chars, start, end),
      "\{kind_name} sequence exceeded the configured length limit",
    )
    end
  } else {
    add_finding(
      findings,
      TruncatedSequence,
      Dangerous,
      start,
      sequence_text(chars, start, length),
      "unterminated \{kind_name} sequence",
    )
    length
  }
}

///|
fn scan_escape(
  chars : Array[Char],
  start : Int,
  policy : Policy,
  output : Array[Char],
  findings : Array[Finding],
) -> Int {
  let length = chars.length()
  if start + 1 >= length {
    add_finding(
      findings,
      TruncatedSequence,
      Dangerous,
      start,
      sequence_text(chars, start, length),
      "trailing escape character",
    )
    return length
  }
  let introducer = chars[start + 1]
  if introducer == '[' {
    scan_csi(chars, start, 2, policy, output, findings)
  } else if introducer == ']' {
    scan_string_control(chars, start, 2, policy, "OSC", output, findings)
  } else if introducer == 'P' {
    scan_string_control(chars, start, 2, policy, "DCS", output, findings)
  } else if introducer == '_' {
    scan_string_control(chars, start, 2, policy, "APC", output, findings)
  } else if introducer == '^' {
    scan_string_control(chars, start, 2, policy, "PM", output, findings)
  } else if introducer == '(' ||
    introducer == ')' ||
    introducer == '*' ||
    introducer == '+' ||
    introducer == '-' ||
    introducer == '.' ||
    introducer == '/' ||
    introducer == '%' {
    if start + 2 >= length {
      add_finding(
        findings,
        TruncatedSequence,
        Dangerous,
        start,
        sequence_text(chars, start, length),
        "truncated character-set selection sequence",
      )
      length
    } else {
      let end = start + 3
      add_finding(
        findings,
        CharacterSet,
        Info,
        start,
        sequence_text(chars, start, end),
        "terminal character-set selection",
      )
      end
    }
  } else if introducer == '#' {
    if start + 2 >= length {
      add_finding(
        findings,
        TruncatedSequence,
        Dangerous,
        start,
        sequence_text(chars, start, length),
        "truncated terminal test sequence",
      )
      length
    } else {
      let end = start + 3
      add_finding(
        findings,
        ScreenControl,
        Warning,
        start,
        sequence_text(chars, start, end),
        "terminal screen-alignment or test sequence",
      )
      end
    }
  } else if introducer == '7' || introducer == '8' {
    let end = start + 2
    add_finding(
      findings,
      CursorControl,
      Warning,
      start,
      sequence_text(chars, start, end),
      "save or restore cursor state",
    )
    end
  } else if introducer == 'D' ||
    introducer == 'E' ||
    introducer == 'M' ||
    introducer == 'c' {
    let end = start + 2
    add_finding(
      findings,
      ScreenControl,
      Warning,
      start,
      sequence_text(chars, start, end),
      "terminal index, reverse-index, newline, or reset operation",
    )
    end
  } else if introducer == '=' || introducer == '>' {
    let end = start + 2
    add_finding(
      findings,
      ModeChange,
      Warning,
      start,
      sequence_text(chars, start, end),
      "terminal keypad mode change",
    )
    end
  } else {
    let end = start + 2
    add_finding(
      findings,
      UnknownEscape,
      Warning,
      start,
      sequence_text(chars, start, end),
      "unknown or two-byte escape sequence",
    )
    end
  }
}

///|
fn scan_c1(
  chars : Array[Char],
  start : Int,
  policy : Policy,
  output : Array[Char],
  findings : Array[Finding],
) -> Int {
  let code = chars[start].to_int()
  if code == 0x9b {
    scan_csi(chars, start, 1, policy, output, findings)
  } else if code == 0x9d {
    scan_string_control(chars, start, 1, policy, "OSC", output, findings)
  } else if code == 0x90 {
    scan_string_control(chars, start, 1, policy, "DCS", output, findings)
  } else if code == 0x9f {
    scan_string_control(chars, start, 1, policy, "APC", output, findings)
  } else if code == 0x9e {
    scan_string_control(chars, start, 1, policy, "PM", output, findings)
  } else {
    add_finding(
      findings,
      ControlCharacter,
      Warning,
      start,
      sequence_text(chars, start, start + 1),
      "C1 control character",
    )
    start + 1
  }
}

///|
/// Sanitizes terminal output and returns safe text plus an audit trail.
pub fn scan_with_policy(input : String, policy : Policy) -> ScanResult {
  let chars = input.to_array()
  let output : Array[Char] = []
  let findings : Array[Finding] = []
  let mut index = 0
  while index < chars.length() {
    let char = chars[index]
    let code = char.to_int()
    if char == '\u{1b}' {
      index = scan_escape(chars, index, policy, output, findings)
    } else if code >= 0x80 && code <= 0x9f {
      index = scan_c1(chars, index, policy, output, findings)
    } else if code < 0x20 || code == 0x7f {
      if char == '\n' && policy.allow_newline {
        output.push(char)
      } else if char == '\t' && policy.allow_tab {
        output.push(char)
      } else {
        let (severity, message) = if char == '\r' || char == '\b' {
          (
            Warning,
            "carriage return or backspace can overwrite visible log text",
          )
        } else {
          (Info, "C0 control character")
        }
        add_finding(
          findings,
          ControlCharacter,
          severity,
          index,
          sequence_text(chars, index, index + 1),
          message,
        )
      }
      index = index + 1
    } else {
      output.push(char)
      index = index + 1
    }
  }
  {
    text: String::from_array(output),
    findings,
    removed_count: findings.length(),
  }
}

///|
/// Sanitizes terminal output with the strict default policy.
pub fn scan(input : String) -> ScanResult {
  scan_with_policy(input, Policy::strict())
}

///|
/// Returns strict plain text without audit metadata.
pub fn sanitize(input : String) -> String {
  scan(input).text()
}