///|
/// Classification for a single CSP source expression.
pub(all) enum SourceExpressionKind {
  SourceEmpty
  SourceNone
  SourceSelf
  SourceUnsafeInline
  SourceUnsafeEval
  SourceWasmUnsafeEval
  SourceStrictDynamic
  SourceUnsafeHashes
  SourceReportSample
  SourceNonce
  SourceSha256
  SourceSha384
  SourceSha512
  SourceScheme
  SourceHost
  SourceWildcard
  SourceData
  SourceBlob
  SourceFilesystem
  SourceMediastream
  SourceHttpsScheme
  SourceHttpScheme
  SourceUpgradeKeyword
  SourceUnknownKeyword
  SourceUnknown
} derive(Eq, Debug)

///|
/// Coarse trust bucket used by reports and scoring.
pub(all) enum SourceTrust {
  TrustBlocksAll
  TrustSameOrigin
  TrustCryptographic
  TrustExplicitOrigin
  TrustTransportScheme
  TrustDynamic
  TrustDangerous
  TrustUnknown
} derive(Eq, Debug)

///|
/// Structured information derived from a raw CSP source expression.
pub(all) struct SourceExpression {
  raw : String
  normalized : String
  kind : SourceExpressionKind
  trust : SourceTrust
  scheme : String?
  host : String?
  port : String?
  path : String?
  description : String
} derive(Eq, Debug)

///|
let source_ch_colon = 58

///|
let source_ch_slash = 47

///|
let source_ch_star = 42

///|
let source_ch_dot = 46

///|
let source_ch_lbracket = 91

///|
let source_ch_rbracket = 93

///|
let source_ch_question = 63

///|
let source_ch_hash = 35

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

///|
fn source_kind_name(kind : SourceExpressionKind) -> String {
  match kind {
    SourceEmpty => "empty"
    SourceNone => "none"
    SourceSelf => "self"
    SourceUnsafeInline => "unsafe-inline"
    SourceUnsafeEval => "unsafe-eval"
    SourceWasmUnsafeEval => "wasm-unsafe-eval"
    SourceStrictDynamic => "strict-dynamic"
    SourceUnsafeHashes => "unsafe-hashes"
    SourceReportSample => "report-sample"
    SourceNonce => "nonce"
    SourceSha256 => "sha256"
    SourceSha384 => "sha384"
    SourceSha512 => "sha512"
    SourceScheme => "scheme"
    SourceHost => "host"
    SourceWildcard => "wildcard"
    SourceData => "data"
    SourceBlob => "blob"
    SourceFilesystem => "filesystem"
    SourceMediastream => "mediastream"
    SourceHttpsScheme => "https-scheme"
    SourceHttpScheme => "http-scheme"
    SourceUpgradeKeyword => "upgrade-keyword"
    SourceUnknownKeyword => "unknown-keyword"
    SourceUnknown => "unknown"
  }
}

///|
pub fn SourceExpressionKind::name(self : SourceExpressionKind) -> String {
  source_kind_name(self)
}

///|
pub fn SourceTrust::name(self : SourceTrust) -> String {
  match self {
    TrustBlocksAll => "blocks-all"
    TrustSameOrigin => "same-origin"
    TrustCryptographic => "cryptographic"
    TrustExplicitOrigin => "explicit-origin"
    TrustTransportScheme => "transport-scheme"
    TrustDynamic => "dynamic"
    TrustDangerous => "dangerous"
    TrustUnknown => "unknown"
  }
}

///|
fn source_description(kind : SourceExpressionKind) -> String {
  match kind {
    SourceEmpty => "Empty source expression."
    SourceNone => "Blocks every source when used alone."
    SourceSelf => "Allows resources from the protected document origin."
    SourceUnsafeInline => "Allows inline code and weakens injection protection."
    SourceUnsafeEval => "Allows string-to-code evaluation."
    SourceWasmUnsafeEval => "Allows dynamic WebAssembly compilation."
    SourceStrictDynamic => "Delegates script trust to nonced or hashed scripts."
    SourceUnsafeHashes => "Allows inline event handlers matched by hashes."
    SourceReportSample => "Requests a source sample in violation reports."
    SourceNonce => "Cryptographic nonce source."
    SourceSha256 => "SHA-256 hash source."
    SourceSha384 => "SHA-384 hash source."
    SourceSha512 => "SHA-512 hash source."
    SourceScheme => "Allows every origin using a scheme."
    SourceHost => "Allows an explicit host source."
    SourceWildcard => "Allows any network source."
    SourceData => "Allows data: URLs."
    SourceBlob => "Allows blob: URLs."
    SourceFilesystem => "Allows filesystem: URLs."
    SourceMediastream => "Allows mediastream: URLs."
    SourceHttpsScheme => "Allows any HTTPS origin."
    SourceHttpScheme => "Allows any plain HTTP origin."
    SourceUpgradeKeyword => "Requests upgrading insecure requests."
    SourceUnknownKeyword => "Quoted keyword not recognized by CSPKit."
    SourceUnknown => "Source expression could not be classified precisely."
  }
}

///|
fn source_trust_for(kind : SourceExpressionKind) -> SourceTrust {
  match kind {
    SourceNone => TrustBlocksAll
    SourceSelf => TrustSameOrigin
    SourceNonce => TrustCryptographic
    SourceSha256 => TrustCryptographic
    SourceSha384 => TrustCryptographic
    SourceSha512 => TrustCryptographic
    SourceHost => TrustExplicitOrigin
    SourceHttpsScheme => TrustTransportScheme
    SourceScheme => TrustTransportScheme
    SourceStrictDynamic => TrustDynamic
    SourceReportSample => TrustDynamic
    SourceUpgradeKeyword => TrustDynamic
    SourceWildcard => TrustDangerous
    SourceUnsafeInline => TrustDangerous
    SourceUnsafeEval => TrustDangerous
    SourceWasmUnsafeEval => TrustDangerous
    SourceUnsafeHashes => TrustDangerous
    SourceData => TrustDangerous
    SourceBlob => TrustDangerous
    SourceFilesystem => TrustDangerous
    SourceMediastream => TrustDangerous
    SourceHttpScheme => TrustDangerous
    SourceEmpty => TrustUnknown
    SourceUnknownKeyword => TrustUnknown
    SourceUnknown => TrustUnknown
  }
}

///|
fn index_of_code(input : String, code : Int) -> Int {
  for i in 0.. Int {
  if start < 0 {
    return index_of_code(input, code)
  }
  for i in start.. Bool {
  index_of_code(input, code) >= 0
}

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

///|
fn contains_text(input : String, needle : String) -> Bool {
  if needle.length() == 0 {
    return true
  }
  if needle.length() > input.length() {
    return false
  }
  for i in 0..<(input.length() - needle.length() + 1) {
    if input[i:i + needle.length()].to_owned() == needle {
      return true
    }
  }
  false
}

///|
fn before_query_or_hash(input : String) -> String {
  let query = index_of_code(input, source_ch_question)
  let hash = index_of_code(input, source_ch_hash)
  if query >= 0 && hash >= 0 {
    let end = if query < hash { query } else { hash }
    input[0:end].to_owned()
  } else if query >= 0 {
    input[0:query].to_owned()
  } else if hash >= 0 {
    input[0:hash].to_owned()
  } else {
    input
  }
}

///|
fn scheme_part(value : String) -> String? {
  let colon = index_of_code(value, source_ch_colon)
  if colon <= 0 {
    None
  } else {
    Some(value[0:colon].to_owned())
  }
}

///|
fn value_after_scheme(value : String) -> String {
  let colon = index_of_code(value, source_ch_colon)
  if colon < 0 || colon + 1 >= value.length() {
    ""
  } else {
    value[colon + 1:value.length()].to_owned()
  }
}

///|
pub fn source_scheme_tail(raw : StringView) -> String {
  value_after_scheme(trim_csp(raw))
}

///|
fn has_scheme_separator(value : String) -> Bool {
  let colon = index_of_code(value, source_ch_colon)
  colon >= 0 &&
  colon + 2 < value.length() &&
  char_at_source(value, colon + 1) == source_ch_slash &&
  char_at_source(value, colon + 2) == source_ch_slash
}

///|
fn path_start_after_authority(value : String) -> Int {
  let colon = index_of_code(value, source_ch_colon)
  if colon >= 0 &&
    colon + 2 < value.length() &&
    char_at_source(value, colon + 1) == source_ch_slash &&
    char_at_source(value, colon + 2) == source_ch_slash {
    let slash = index_of_code_from(value, source_ch_slash, colon + 3)
    slash
  } else {
    index_of_code(value, source_ch_slash)
  }
}

///|
fn authority_part(value : String) -> String {
  let clean = before_query_or_hash(value)
  let start = if has_scheme_separator(clean) {
    index_of_code(clean, source_ch_colon) + 3
  } else {
    0
  }
  let slash = index_of_code_from(clean, source_ch_slash, start)
  if slash >= 0 {
    clean[start:slash].to_owned()
  } else {
    clean[start:clean.length()].to_owned()
  }
}

///|
fn path_part(value : String) -> String? {
  let clean = before_query_or_hash(value)
  let start = path_start_after_authority(clean)
  if start >= 0 && start < clean.length() {
    Some(clean[start:clean.length()].to_owned())
  } else {
    None
  }
}

///|
fn split_host_port(authority : String) -> (String?, String?) {
  if authority.length() == 0 {
    return (None, None)
  }
  if char_at_source(authority, 0) == source_ch_lbracket {
    let close = index_of_code(authority, source_ch_rbracket)
    if close > 0 {
      let host = authority[0:close + 1].to_owned()
      if close + 2 < authority.length() &&
        char_at_source(authority, close + 1) == source_ch_colon {
        return (
          Some(host),
          Some(authority[close + 2:authority.length()].to_owned()),
        )
      }
      return (Some(host), None)
    }
  }
  let colon = index_of_code(authority, source_ch_colon)
  if colon > 0 {
    (
      Some(authority[0:colon].to_owned()),
      Some(authority[colon + 1:authority.length()].to_owned()),
    )
  } else {
    (Some(authority), None)
  }
}

///|
fn quoted_keyword_kind(lower : String) -> SourceExpressionKind? {
  if lower == "'none'" {
    Some(SourceNone)
  } else if lower == "'self'" {
    Some(SourceSelf)
  } else if lower == "'unsafe-inline'" {
    Some(SourceUnsafeInline)
  } else if lower == "'unsafe-eval'" {
    Some(SourceUnsafeEval)
  } else if lower == "'wasm-unsafe-eval'" {
    Some(SourceWasmUnsafeEval)
  } else if lower == "'strict-dynamic'" {
    Some(SourceStrictDynamic)
  } else if lower == "'unsafe-hashes'" {
    Some(SourceUnsafeHashes)
  } else if lower == "'report-sample'" {
    Some(SourceReportSample)
  } else {
    None
  }
}

///|
fn hash_kind(lower : String) -> SourceExpressionKind? {
  if starts_with(lower, "'sha256-") && ends_with_text(lower, "'") {
    Some(SourceSha256)
  } else if starts_with(lower, "'sha384-") && ends_with_text(lower, "'") {
    Some(SourceSha384)
  } else if starts_with(lower, "'sha512-") && ends_with_text(lower, "'") {
    Some(SourceSha512)
  } else {
    None
  }
}

///|
fn scheme_only_kind(lower : String) -> SourceExpressionKind? {
  if lower == "http:" {
    Some(SourceHttpScheme)
  } else if lower == "https:" {
    Some(SourceHttpsScheme)
  } else if lower == "data:" {
    Some(SourceData)
  } else if lower == "blob:" {
    Some(SourceBlob)
  } else if lower == "filesystem:" {
    Some(SourceFilesystem)
  } else if lower == "mediastream:" {
    Some(SourceMediastream)
  } else if ends_with_text(lower, ":") {
    Some(SourceScheme)
  } else {
    None
  }
}

///|
fn classify_source_kind(raw : String, lower : String) -> SourceExpressionKind {
  if raw.length() == 0 {
    return SourceEmpty
  }
  if raw.length() == 1 && char_at_source(raw, 0) == source_ch_star {
    return SourceWildcard
  }
  match quoted_keyword_kind(lower) {
    Some(kind) => return kind
    None => ()
  }
  if starts_with(lower, "'nonce-") && ends_with_text(lower, "'") {
    return SourceNonce
  }
  match hash_kind(lower) {
    Some(kind) => return kind
    None => ()
  }
  match scheme_only_kind(lower) {
    Some(kind) => return kind
    None => ()
  }
  if lower == "upgrade-insecure-requests" {
    return SourceUpgradeKeyword
  }
  if starts_with(lower, "'") && ends_with_text(lower, "'") {
    return SourceUnknownKeyword
  }
  if has_scheme_separator(lower) ||
    contains_code(lower, source_ch_dot) ||
    starts_with(lower, "*.") {
    return SourceHost
  }
  SourceUnknown
}

///|
pub fn classify_source_expression(raw : StringView) -> SourceExpression {
  let value = trim_csp(raw)
  let lower = lower_ascii(value[:])
  let kind = classify_source_kind(value, lower)
  let scheme = scheme_part(lower)
  let authority = authority_part(value)
  let (host, port) = split_host_port(authority)
  let path = path_part(value)
  let trust = source_trust_for(kind)
  let description = source_description(kind)
  {
    raw: value,
    normalized: lower,
    kind,
    trust,
    scheme,
    host,
    port,
    path,
    description,
  }
}

///|
pub fn SourceExpression::is_empty(self : SourceExpression) -> Bool {
  self.kind == SourceEmpty
}

///|
pub fn SourceExpression::is_keyword(self : SourceExpression) -> Bool {
  self.kind == SourceNone ||
  self.kind == SourceSelf ||
  self.kind == SourceUnsafeInline ||
  self.kind == SourceUnsafeEval ||
  self.kind == SourceWasmUnsafeEval ||
  self.kind == SourceStrictDynamic ||
  self.kind == SourceUnsafeHashes ||
  self.kind == SourceReportSample
}

///|
pub fn SourceExpression::is_cryptographic(self : SourceExpression) -> Bool {
  self.kind == SourceNonce ||
  self.kind == SourceSha256 ||
  self.kind == SourceSha384 ||
  self.kind == SourceSha512
}

///|
pub fn SourceExpression::is_network_source(self : SourceExpression) -> Bool {
  self.kind == SourceHost ||
  self.kind == SourceScheme ||
  self.kind == SourceHttpsScheme ||
  self.kind == SourceHttpScheme ||
  self.kind == SourceWildcard
}

///|
pub fn SourceExpression::is_local_like(self : SourceExpression) -> Bool {
  self.kind == SourceSelf || self.trust == TrustCryptographic
}

///|
pub fn SourceExpression::is_dangerous(self : SourceExpression) -> Bool {
  self.trust == TrustDangerous
}

///|
pub fn SourceExpression::is_dynamic(self : SourceExpression) -> Bool {
  self.trust == TrustDynamic
}

///|
pub fn SourceExpression::uses_plain_http(self : SourceExpression) -> Bool {
  self.kind == SourceHttpScheme ||
  self.scheme is Some("http") ||
  starts_with(self.normalized, "http://")
}

///|
pub fn SourceExpression::allows_any_origin(self : SourceExpression) -> Bool {
  self.kind == SourceWildcard ||
  self.normalized == "https:" ||
  self.normalized == "http:" ||
  self.normalized == "*:"
}

///|
pub fn SourceExpression::allows_data_like_url(self : SourceExpression) -> Bool {
  self.kind == SourceData ||
  self.kind == SourceBlob ||
  self.kind == SourceFilesystem ||
  self.kind == SourceMediastream
}

///|
pub fn SourceExpression::is_wildcard_host(self : SourceExpression) -> Bool {
  starts_with(self.normalized, "*.") || contains_text(self.normalized, "://*.")
}

///|
pub fn SourceExpression::is_host_with_path(self : SourceExpression) -> Bool {
  self.kind == SourceHost && self.path is Some(_)
}

///|
pub fn SourceExpression::has_port(self : SourceExpression) -> Bool {
  self.port is Some(_)
}

///|
pub fn SourceExpression::has_scheme(self : SourceExpression) -> Bool {
  self.scheme is Some(_)
}

///|
pub fn SourceExpression::summary(self : SourceExpression) -> String {
  self.raw + " kind=" + self.kind.name() + " trust=" + self.trust.name()
}

///|
pub fn SourceExpression::detail(self : SourceExpression) -> String {
  let scheme = match self.scheme {
    Some(scheme) => scheme
    None => "-"
  }
  let host = match self.host {
    Some(host) => host
    None => "-"
  }
  let port = match self.port {
    Some(port) => port
    None => "-"
  }
  let path = match self.path {
    Some(path) => path
    None => "-"
  }
  self.summary() +
  " scheme=" +
  scheme +
  " host=" +
  host +
  " port=" +
  port +
  " path=" +
  path
}

///|
pub fn Directive::source_expressions(
  self : Directive,
) -> Array[SourceExpression] {
  self.values.map(value => classify_source_expression(value[:]))
}

///|
pub fn Directive::dangerous_sources(
  self : Directive,
) -> Array[SourceExpression] {
  self.source_expressions().filter(source => source.is_dangerous())
}

///|
pub fn Directive::network_sources(self : Directive) -> Array[SourceExpression] {
  self.source_expressions().filter(source => source.is_network_source())
}

///|
pub fn Directive::cryptographic_sources(
  self : Directive,
) -> Array[SourceExpression] {
  self.source_expressions().filter(source => source.is_cryptographic())
}

///|
pub fn Directive::has_source_kind(
  self : Directive,
  kind : SourceExpressionKind,
) -> Bool {
  for source in self.source_expressions() {
    if source.kind == kind {
      return true
    }
  }
  false
}

///|
pub fn Directive::has_dangerous_source(self : Directive) -> Bool {
  for source in self.source_expressions() {
    if source.is_dangerous() {
      return true
    }
  }
  false
}

///|
pub fn Directive::has_cryptographic_source(self : Directive) -> Bool {
  for source in self.source_expressions() {
    if source.is_cryptographic() {
      return true
    }
  }
  false
}

///|
pub fn Directive::has_network_source(self : Directive) -> Bool {
  for source in self.source_expressions() {
    if source.is_network_source() {
      return true
    }
  }
  false
}

///|
pub fn Directive::source_summary(self : Directive) -> String {
  let rows : Array[String] = []
  for source in self.source_expressions() {
    rows.push(source.summary())
  }
  rows.join("\n")
}

///|
pub fn Policy::all_sources(self : Policy) -> Array[SourceExpression] {
  let out : Array[SourceExpression] = []
  for directive in self.directives {
    for source in directive.source_expressions() {
      out.push(source)
    }
  }
  out
}

///|
pub fn Policy::dangerous_sources(self : Policy) -> Array[SourceExpression] {
  self.all_sources().filter(source => source.is_dangerous())
}

///|
pub fn Policy::cryptographic_sources(self : Policy) -> Array[SourceExpression] {
  self.all_sources().filter(source => source.is_cryptographic())
}

///|
pub fn Policy::network_sources(self : Policy) -> Array[SourceExpression] {
  self.all_sources().filter(source => source.is_network_source())
}

///|
pub fn Policy::source_kind_count(
  self : Policy,
  kind : SourceExpressionKind,
) -> Int {
  let mut count = 0
  for source in self.all_sources() {
    if source.kind == kind {
      count = count + 1
    }
  }
  count
}

///|
pub fn Policy::has_source_kind(
  self : Policy,
  kind : SourceExpressionKind,
) -> Bool {
  self.source_kind_count(kind) > 0
}

///|
pub fn Policy::uses_nonce_or_hash(self : Policy) -> Bool {
  for source in self.all_sources() {
    if source.is_cryptographic() {
      return true
    }
  }
  false
}

///|
pub fn Policy::uses_plain_http(self : Policy) -> Bool {
  for source in self.all_sources() {
    if source.uses_plain_http() {
      return true
    }
  }
  false
}

///|
pub fn Policy::allows_any_origin(self : Policy) -> Bool {
  for source in self.all_sources() {
    if source.allows_any_origin() {
      return true
    }
  }
  false
}

///|
pub fn Policy::source_inventory_report(self : Policy) -> String {
  let lines : Array[String] = []
  lines.push("sources=\{self.all_sources().length()}")
  lines.push("dangerous=\{self.dangerous_sources().length()}")
  lines.push("network=\{self.network_sources().length()}")
  lines.push("cryptographic=\{self.cryptographic_sources().length()}")
  lines.join("\n")
}