///|
/// CSPKit is a MoonBit-native Content-Security-Policy parser and auditor.
///
/// It focuses on the security policy text itself, not on HTTP transport. The
/// core API parses a CSP header into structured directives, computes effective
/// fetch-source fallback, reports risky patterns, and compares two policies.

///|
pub(all) enum ParseError {
  EmptyPolicy
  EmptyDirective
  InvalidDirectiveName(name~ : String)
  InvalidDirectiveValue(value~ : String)
} derive(Eq, Debug)

///|
pub(all) enum Severity {
  Info
  Warning
  High
} derive(Eq, Debug)

///|
pub(all) struct Directive {
  name : String
  values : Array[String]
  order : Int
} derive(Eq, Debug)

///|
pub(all) struct Policy {
  directives : Array[Directive]
} derive(Eq, Debug)

///|
pub(all) struct Finding {
  code : String
  severity : Severity
  directive : String?
  message : String
} derive(Eq, Debug)

///|
pub(all) struct DirectiveChange {
  name : String
  before : Array[String]
  after : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct PolicyDiff {
  added : Array[Directive]
  removed : Array[Directive]
  changed : Array[DirectiveChange]
} derive(Eq, Debug)

///|
let ch_space = 32

///|
let ch_tab = 9

///|
let ch_lf = 10

///|
let ch_cr = 13

///|
let ch_semicolon = 59

///|
fn code_at(input : String, index : Int) -> Int {
  input.unsafe_get(index).to_int()
}

///|
fn trim_csp(input : StringView) -> String {
  input.trim(chars=" \t\n\r").to_owned()
}

///|
fn lower_ascii(input : StringView) -> String {
  let buf = StringBuilder(size_hint=input.length())
  for c in input {
    buf.write_char(c.to_ascii_lowercase())
  }
  buf.to_string()
}

///|
fn is_alpha(n : Int) -> Bool {
  (n >= 65 && n <= 90) || (n >= 97 && n <= 122)
}

///|
fn is_digit(n : Int) -> Bool {
  n >= 48 && n <= 57
}

///|
fn is_ws(n : Int) -> Bool {
  n == ch_space || n == ch_tab || n == ch_lf || n == ch_cr
}

///|
fn is_directive_name_char(n : Int) -> Bool {
  is_alpha(n) || is_digit(n) || n == 45
}

///|
fn is_visible_ascii(n : Int) -> Bool {
  n >= 33 && n <= 126
}

///|
fn starts_with(input : String, prefix : String) -> Bool {
  prefix.length() <= input.length() &&
  input[0:prefix.length()].to_owned() == prefix
}

///|
fn split_directives(input : String) -> Array[String] {
  let parts : Array[String] = []
  let mut start = 0
  for i in 0.. Array[String] {
  let tokens : Array[String] = []
  let mut start = -1
  for i in 0..= 0 {
        tokens.push(input[start:i].to_owned())
        start = -1
      }
    } else if start < 0 {
      start = i
    }
  }
  if start >= 0 {
    tokens.push(input[start:input.length()].to_owned())
  }
  tokens
}

///|
fn normalize_directive_name(raw : StringView) -> Result[String, ParseError] {
  let name = lower_ascii(raw.trim(chars=" \t\n\r"))
  guard name.length() > 0 else { return Err(EmptyDirective) }
  for i in 0.. Result[String, ParseError] {
  guard value.length() > 0 else { return Err(InvalidDirectiveValue(value~)) }
  for i in 0.. Result[Directive, ParseError] {
  let segment = trim_csp(raw[:])
  guard segment.length() > 0 else { return Err(EmptyDirective) }
  let tokens = split_ws(segment)
  guard tokens.length() > 0 else { return Err(EmptyDirective) }
  let name = match normalize_directive_name(tokens[0][:]) {
    Ok(name) => name
    Err(err) => return Err(err)
  }
  let values : Array[String] = []
  for i in 1.. values.push(value)
      Err(err) => return Err(err)
    }
  }
  Ok({ name, values, order })
}

///|
pub fn parse_policy(input : StringView) -> Result[Policy, ParseError] {
  let raw = trim_csp(input)
  guard raw.length() > 0 else { return Err(EmptyPolicy) }
  let directives : Array[Directive] = []
  for part in split_directives(raw) {
    let segment = trim_csp(part[:])
    if segment.length() > 0 {
      match parse_directive(segment, directives.length()) {
        Ok(directive) => directives.push(directive)
        Err(err) => return Err(err)
      }
    }
  }
  guard directives.length() > 0 else { return Err(EmptyPolicy) }
  Ok({ directives, })
}

///|
pub fn Policy::directive(self : Policy, name : StringView) -> Directive? {
  let key = lower_ascii(name)
  for directive in self.directives {
    if directive.name == key {
      return Some(directive)
    }
  }
  None
}

///|
pub fn Policy::directives_named(
  self : Policy,
  name : StringView,
) -> Array[Directive] {
  let key = lower_ascii(name)
  let matches : Array[Directive] = []
  for directive in self.directives {
    if directive.name == key {
      matches.push(directive)
    }
  }
  matches
}

///|
pub fn Policy::contains(self : Policy, name : StringView) -> Bool {
  self.directive(name) is Some(_)
}

///|
pub fn Directive::contains(self : Directive, value : StringView) -> Bool {
  let key = lower_ascii(value)
  for item in self.values {
    if lower_ascii(item[:]) == key {
      return true
    }
  }
  false
}

///|
fn is_fetch_directive(name : String) -> Bool {
  name == "child-src" ||
  name == "connect-src" ||
  name == "default-src" ||
  name == "font-src" ||
  name == "frame-src" ||
  name == "img-src" ||
  name == "manifest-src" ||
  name == "media-src" ||
  name == "object-src" ||
  name == "prefetch-src" ||
  name == "script-src" ||
  name == "script-src-elem" ||
  name == "script-src-attr" ||
  name == "style-src" ||
  name == "style-src-elem" ||
  name == "style-src-attr" ||
  name == "worker-src"
}

///|
pub fn Policy::effective_sources(
  self : Policy,
  name : StringView,
) -> Array[String]? {
  let key = lower_ascii(name)
  match self.directive(key[:]) {
    Some(directive) => Some(directive.values)
    None =>
      if is_fetch_directive(key) {
        match self.directive("default-src") {
          Some(default_src) => Some(default_src.values)
          None => None
        }
      } else {
        None
      }
  }
}

///|
fn finding(
  code : String,
  severity : Severity,
  directive : String?,
  message : String,
) -> Finding {
  { code, severity, directive, message }
}

///|
fn add_if_missing(
  findings : Array[Finding],
  policy : Policy,
  name : String,
  severity : Severity,
  message : String,
) -> Unit {
  if !policy.contains(name[:]) {
    findings.push(finding("missing-" + name, severity, Some(name), message))
  }
}

///|
fn has_more_than_one(values : Array[String]) -> Bool {
  values.length() > 1
}

///|
fn audit_directive_values(
  findings : Array[Finding],
  directive : Directive,
) -> Unit {
  if directive.contains("'none'") && has_more_than_one(directive.values) {
    findings.push(
      finding(
        "none-with-other-sources",
        Warning,
        Some(directive.name),
        "'none' should not be mixed with other source expressions.",
      ),
    )
  }
  for value in directive.values {
    let lower = lower_ascii(value[:])
    if lower == "*" {
      findings.push(
        finding(
          "wildcard-source",
          High,
          Some(directive.name),
          "Wildcard source allows any origin for this directive.",
        ),
      )
    }
    if lower == "'unsafe-inline'" {
      let severity = if starts_with(directive.name, "script-src") {
        High
      } else {
        Warning
      }
      findings.push(
        finding(
          "unsafe-inline",
          severity,
          Some(directive.name),
          "'unsafe-inline' weakens injection protection.",
        ),
      )
    }
    if lower == "'unsafe-eval'" || lower == "'wasm-unsafe-eval'" {
      findings.push(
        finding(
          "unsafe-eval",
          High,
          Some(directive.name),
          "Dynamic code evaluation is allowed.",
        ),
      )
    }
    if lower == "data:" &&
      (
        starts_with(directive.name, "script-src") ||
        directive.name == "object-src" ||
        directive.name == "default-src"
      ) {
      findings.push(
        finding(
          "data-source-sensitive-context",
          High,
          Some(directive.name),
          "data: is risky for script, object, or default sources.",
        ),
      )
    }
    if lower == "http:" || starts_with(lower, "http://") {
      findings.push(
        finding(
          "insecure-http-source",
          Warning,
          Some(directive.name),
          "Plain HTTP source can be modified in transit.",
        ),
      )
    }
    if starts_with(lower, "'nonce-") ||
      starts_with(lower, "'sha256-") ||
      starts_with(lower, "'sha384-") ||
      starts_with(lower, "'sha512-") {
      findings.push(
        finding(
          "strict-source-token",
          Info,
          Some(directive.name),
          "Nonce or hash source expression is present.",
        ),
      )
    }
  }
}

///|
fn audit_duplicates(policy : Policy, findings : Array[Finding]) -> Unit {
  for i in 0.. Array[Finding] {
  let findings : Array[Finding] = []
  audit_duplicates(self, findings)
  add_if_missing(
    findings,
    self,
    "default-src",
    High,
    "default-src is the baseline fallback for fetch directives.",
  )
  add_if_missing(
    findings,
    self,
    "object-src",
    Warning,
    "object-src 'none' is recommended for modern web applications.",
  )
  add_if_missing(
    findings,
    self,
    "base-uri",
    Warning,
    "base-uri limits attacker-controlled base tag injection.",
  )
  add_if_missing(
    findings,
    self,
    "frame-ancestors",
    Warning,
    "frame-ancestors protects against unwanted embedding.",
  )
  for directive in self.directives {
    audit_directive_values(findings, directive)
  }
  findings
}

///|
fn values_equal(left : Array[String], right : Array[String]) -> Bool {
  guard left.length() == right.length() else { return false }
  for i in 0.. PolicyDiff {
  let added : Array[Directive] = []
  let removed : Array[Directive] = []
  let changed : Array[DirectiveChange] = []
  for new_directive in after.directives {
    match before.directive(new_directive.name[:]) {
      None => added.push(new_directive)
      Some(old_directive) =>
        if !values_equal(old_directive.values, new_directive.values) {
          changed.push({
            name: new_directive.name,
            before: old_directive.values,
            after: new_directive.values,
          })
        }
    }
  }
  for old_directive in before.directives {
    if after.directive(old_directive.name[:]) is None {
      removed.push(old_directive)
    }
  }
  { added, removed, changed }
}

///|
pub fn Directive::to_header(self : Directive) -> String {
  if self.values.length() == 0 {
    self.name
  } else {
    self.name + " " + self.values.join(" ")
  }
}

///|
pub fn Policy::to_header(self : Policy) -> String {
  self.directives.map(directive => directive.to_header()).join("; ")
}

///|
pub fn Finding::is_blocking(self : Finding) -> Bool {
  self.severity == High
}

///|
pub fn Policy::blocking_findings(self : Policy) -> Array[Finding] {
  self.audit().filter(f => f.is_blocking())
}