///|
/// One normalized Cache-Control directive. Unknown extensions are represented
/// exactly like registered directives so callers can inspect them.
pub(all) struct CacheDirective {
  name : String
  value : String?
  quoted : Bool
  raw : String
} derive(Debug, Eq)

///|
/// Loss-tolerant parse result for one or more Cache-Control field lines.
pub(all) struct CacheControl {
  directives : Array[CacheDirective]
  diagnostics : Array[Diagnostic]
} derive(Debug, Eq)

///|
/// Result of reading a numeric delta-seconds directive.
pub(all) enum DeltaDirective {
  DeltaMissing
  DeltaValid(Int64)
  DeltaInvalid
  DeltaRepeated
} derive(Debug, Eq)

///|
/// Parse every Cache-Control field line. Syntax faults are captured as
/// diagnostics and valid neighboring directives remain available.
pub fn parse_cache_control(headers : Headers) -> CacheControl {
  let directives : Array[CacheDirective] = []
  let diagnostics = headers.diagnostics()
  for field_value in headers.values("cache-control") {
    let split = @lex.split_quoted_list(field_value)
    if !split.valid {
      diagnostics.push(
        diagnostic(
          Error,
          "CACHE_CONTROL_UNCLOSED_QUOTE",
          "Cache-Control contains an unclosed quoted string",
        ),
      )
    }
    for raw in split.parts {
      parse_directive(raw, directives, diagnostics)
    }
  }
  append_duplicate_diagnostics(directives, diagnostics)
  { directives, diagnostics, }
}

///|
fn parse_directive(
  raw : String,
  directives : Array[CacheDirective],
  diagnostics : Array[Diagnostic],
) -> Unit {
  let text = @lex.trim_ows(raw)
  if text.length() == 0 {
    diagnostics.push(
      diagnostic(
        Warning,
        "CACHE_CONTROL_EMPTY_MEMBER",
        "empty Cache-Control list member was ignored",
      ),
    )
    return
  }
  match @lex.find_unquoted_equals(text) {
    None => {
      let name = @lex.lower(text)
      if !@lex.is_token(name) {
        diagnostics.push(
          diagnostic(
            Error,
            "CACHE_CONTROL_NAME_INVALID",
            "directive name is not a valid HTTP token",
          ),
        )
        return
      }
      directives.push({ name, value: None, quoted: false, raw: text, })
    }
    Some(offset) => {
      let name = @lex.lower(text[:offset].trim(chars=" \t").to_owned())
      let encoded = text[offset + 1:].trim(chars=" \t").to_owned()
      if !@lex.is_token(name) {
        diagnostics.push(
          diagnostic(
            Error,
            "CACHE_CONTROL_NAME_INVALID",
            "directive name is not a valid HTTP token",
          ),
        )
        return
      }
      if encoded.length() == 0 {
        diagnostics.push(
          diagnostic(
            Error,
            "CACHE_CONTROL_VALUE_MISSING",
            "directive has an equals sign but no value",
          ),
        )
        directives.push({ name, value: None, quoted: false, raw: text, })
        return
      }
      if encoded.has_prefix("\"") {
        let decoded = @lex.decode_quoted(encoded)
        if !decoded.valid {
          diagnostics.push(
            diagnostic(
              Error,
              "CACHE_CONTROL_QUOTED_VALUE_INVALID",
              "directive contains an invalid quoted-string",
            ),
          )
        }
        directives.push({
          name,
          value: Some(decoded.value),
          quoted: true,
          raw: text,
        })
      } else if @lex.is_token(encoded) {
        directives.push({
          name,
          value: Some(encoded),
          quoted: false,
          raw: text,
        })
      } else {
        diagnostics.push(
          diagnostic(
            Error,
            "CACHE_CONTROL_TOKEN_VALUE_INVALID",
            "unquoted directive value is not a valid HTTP token",
          ),
        )
        directives.push({
          name,
          value: Some(encoded),
          quoted: false,
          raw: text,
        })
      }
    }
  }
}

///|
fn append_duplicate_diagnostics(
  directives : Array[CacheDirective],
  diagnostics : Array[Diagnostic],
) -> Unit {
  for index, directive in directives {
    let mut seen_before = false
    for prior = 0; prior < index; prior = prior + 1 {
      if directives[prior].name == directive.name {
        seen_before = true
        break
      }
    }
    if seen_before {
      diagnostics.push(
        diagnostic(
          Warning,
          "CACHE_CONTROL_DUPLICATE_DIRECTIVE",
          "duplicate Cache-Control directive: " + directive.name,
        ),
      )
    }
  }
}

///|
fn diagnostic(
  level : DiagnosticLevel,
  code : String,
  message : String,
) -> Diagnostic {
  { level, code, message, field_name: Some("cache-control"), }
}

///|
pub fn CacheControl::contains(self : CacheControl, name : String) -> Bool {
  let normalized = @lex.lower(name)
  for directive in self.directives {
    if directive.name == normalized {
      return true
    }
  }
  false
}

///|
pub fn CacheControl::occurrences(
  self : CacheControl,
  name : String,
) -> Array[CacheDirective] {
  let normalized = @lex.lower(name)
  let matches : Array[CacheDirective] = []
  for directive in self.directives {
    if directive.name == normalized {
      matches.push(directive)
    }
  }
  matches
}

///|
/// Read a single delta-seconds directive with RFC 9111 overflow saturation.
pub fn CacheControl::delta(
  self : CacheControl,
  name : String,
) -> DeltaDirective {
  let matches = self.occurrences(name)
  if matches.length() == 0 {
    return DeltaMissing
  }
  if matches.length() > 1 {
    return DeltaRepeated
  }
  match matches[0].value {
    None => DeltaInvalid
    Some(value) =>
      match @lex.parse_saturating_decimal(value, 2147483648L) {
        Some(seconds) => DeltaValid(seconds)
        None => DeltaInvalid
      }
  }
}

///|
/// Return field-name arguments from directives such as private="set-cookie".
pub fn CacheControl::field_names(
  self : CacheControl,
  name : String,
) -> Array[String] {
  let names : Array[String] = []
  for directive in self.occurrences(name) {
    match directive.value {
      None => ()
      Some(value) => {
        let split = @lex.split_quoted_list(value)
        for item in split.parts {
          let normalized = @lex.lower(@lex.trim_ows(item))
          if @lex.is_token(normalized) {
            names.push(normalized)
          }
        }
      }
    }
  }
  names
}