///|
/// Severity assigned to an audit finding.
pub(all) enum Severity {
  Info
  Warning
  Critical
} derive(Eq, Debug)

///|
pub fn Severity::label(self : Severity) -> String {
  match self {
    Info => "info"
    Warning => "warning"
    Critical => "critical"
  }
}

///|
pub fn Severity::score(self : Severity) -> Int {
  match self {
    Info => 1
    Warning => 4
    Critical => 10
  }
}

///|
/// Syntax problems found while parsing one logfmt line.
pub(all) enum ParseErrorKind {
  EmptyKey
  InvalidKey
  UnexpectedEquals
  UnterminatedQuote
  BareQuote
} derive(Eq, Debug)

///|
pub fn ParseErrorKind::label(self : ParseErrorKind) -> String {
  match self {
    EmptyKey => "empty_key"
    InvalidKey => "invalid_key"
    UnexpectedEquals => "unexpected_equals"
    UnterminatedQuote => "unterminated_quote"
    BareQuote => "bare_quote"
  }
}

///|
pub struct ParseError {
  kind : ParseErrorKind
  message : String
  offset : Int
} derive(Eq, Debug)

///|
pub fn ParseError::kind(self : ParseError) -> ParseErrorKind {
  self.kind
}

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

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

///|
/// One parsed logfmt field.
pub struct Field {
  key : String
  value : String
  quoted : Bool
  flag : Bool
  offset : Int
} derive(Eq, Debug)

///|
pub fn Field::new(
  key : String,
  value : String,
  quoted? : Bool = false,
  flag? : Bool = false,
  offset? : Int = 0,
) -> Field {
  { key, value, quoted, flag, offset }
}

///|
pub fn Field::key(self : Field) -> String {
  self.key
}

///|
pub fn Field::value(self : Field) -> String {
  self.value
}

///|
pub fn Field::quoted(self : Field) -> Bool {
  self.quoted
}

///|
pub fn Field::is_flag(self : Field) -> Bool {
  self.flag
}

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

///|
pub fn Field::pair(self : Field) -> String {
  self.key + "=" + self.value
}

///|
pub struct ParseResult {
  source : String
  fields : Array[Field]
  errors : Array[ParseError]
} derive(Debug)

///|
pub fn ParseResult::source(self : ParseResult) -> String {
  self.source
}

///|
pub fn ParseResult::fields(self : ParseResult) -> Array[Field] {
  self.fields
}

///|
pub fn ParseResult::errors(self : ParseResult) -> Array[ParseError] {
  self.errors
}

///|
pub fn ParseResult::field_count(self : ParseResult) -> Int {
  self.fields.length()
}

///|
pub fn ParseResult::error_count(self : ParseResult) -> Int {
  self.errors.length()
}

///|
pub fn ParseResult::is_valid(self : ParseResult) -> Bool {
  self.errors.length() == 0
}

///|
pub fn ParseResult::get(self : ParseResult, key : String) -> String {
  for field in self.fields {
    if field.key() == key {
      return field.value()
    }
  }
  ""
}

///|
pub fn ParseResult::has_key(self : ParseResult, key : String) -> Bool {
  for field in self.fields {
    if field.key() == key {
      return true
    }
  }
  false
}

///|
pub fn ParseResult::summary(self : ParseResult) -> String {
  "fields=\{self.field_count()} errors=\{self.error_count()}"
}

///|
pub fn ParseResult::normalized(self : ParseResult) -> String {
  let parts : Array[String] = []
  for field in self.fields {
    if field.is_flag() {
      parts.push(field.key())
    } else if needs_quote(field.value()) {
      parts.push(field.key() + "=\"" + escape_logfmt(field.value()) + "\"")
    } else {
      parts.push(field.pair())
    }
  }
  parts.join(" ")
}

///|
/// Audit finding kinds for logfmt quality and CI review.
pub(all) enum FindingKind {
  SyntaxError
  DuplicateKey
  RequiredKeyMissing
  TooManyFields
  ValueTooLong
  BlankValue
  FlagField
  ControlCharacter
} derive(Eq, Debug)

///|
pub fn FindingKind::label(self : FindingKind) -> String {
  match self {
    SyntaxError => "syntax_error"
    DuplicateKey => "duplicate_key"
    RequiredKeyMissing => "required_key_missing"
    TooManyFields => "too_many_fields"
    ValueTooLong => "value_too_long"
    BlankValue => "blank_value"
    FlagField => "flag_field"
    ControlCharacter => "control_character"
  }
}

///|
pub struct Finding {
  kind : FindingKind
  severity : Severity
  key : String
  message : String
  offset : Int
} derive(Eq, Debug)

///|
pub fn Finding::new(
  kind : FindingKind,
  severity : Severity,
  key : String,
  message : String,
  offset? : Int = 0,
) -> Finding {
  { kind, severity, key, message, offset }
}

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

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

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

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

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

///|
pub struct AuditPolicy {
  max_fields : Int
  max_value_length : Int
  flag_duplicate_keys : Bool
  flag_blank_values : Bool
  flag_flag_fields : Bool
  required_keys : Array[String]
} derive(Debug)

///|
pub fn AuditPolicy::default() -> AuditPolicy {
  {
    max_fields: 24,
    max_value_length: 120,
    flag_duplicate_keys: true,
    flag_blank_values: true,
    flag_flag_fields: true,
    required_keys: [],
  }
}

///|
pub fn AuditPolicy::ci() -> AuditPolicy {
  {
    max_fields: 16,
    max_value_length: 80,
    flag_duplicate_keys: true,
    flag_blank_values: true,
    flag_flag_fields: true,
    required_keys: ["level", "msg"],
  }
}

///|
pub fn AuditPolicy::relaxed() -> AuditPolicy {
  {
    max_fields: 48,
    max_value_length: 240,
    flag_duplicate_keys: true,
    flag_blank_values: false,
    flag_flag_fields: false,
    required_keys: [],
  }
}

///|
pub fn AuditPolicy::with_required_keys(
  self : AuditPolicy,
  keys : Array[String],
) -> AuditPolicy {
  {
    max_fields: self.max_fields,
    max_value_length: self.max_value_length,
    flag_duplicate_keys: self.flag_duplicate_keys,
    flag_blank_values: self.flag_blank_values,
    flag_flag_fields: self.flag_flag_fields,
    required_keys: keys,
  }
}

///|
pub struct AuditReport {
  parsed : ParseResult
  findings : Array[Finding]
} derive(Debug)

///|
pub fn AuditReport::parsed(self : AuditReport) -> ParseResult {
  self.parsed
}

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

///|
pub fn AuditReport::finding_count(self : AuditReport) -> Int {
  self.findings.length()
}

///|
pub fn AuditReport::count_severity(
  self : AuditReport,
  severity : Severity,
) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.severity() == severity {
      count = count + 1
    }
  }
  count
}

///|
pub fn AuditReport::critical_count(self : AuditReport) -> Int {
  self.count_severity(Critical)
}

///|
pub fn AuditReport::warning_count(self : AuditReport) -> Int {
  self.count_severity(Warning)
}

///|
pub fn AuditReport::info_count(self : AuditReport) -> Int {
  self.count_severity(Info)
}

///|
pub fn AuditReport::risk_score(self : AuditReport) -> Int {
  let mut score = 0
  for finding in self.findings {
    score = score + finding.severity().score()
  }
  score
}

///|
pub fn AuditReport::risk_level(self : AuditReport) -> String {
  let score = self.risk_score()
  if self.critical_count() > 0 || score >= 30 {
    "high"
  } else if score >= 10 {
    "medium"
  } else if score > 0 {
    "low"
  } else {
    "clean"
  }
}

///|
pub fn AuditReport::recommended_action(self : AuditReport) -> String {
  if self.critical_count() > 0 {
    "fix_before_release"
  } else if self.warning_count() > 0 {
    "review"
  } else {
    "accept"
  }
}

///|
pub fn AuditReport::text_report(self : AuditReport) -> String {
  let mut output = "MoonLogfmt Lens report\n"
  output = output + "fields: " + self.parsed.field_count().to_string() + "\n"
  output = output + "findings: " + self.finding_count().to_string() + "\n"
  output = output +
    "risk: " +
    self.risk_level() +
    " (score=" +
    self.risk_score().to_string() +
    ")\n"
  output = output + "recommendation: " + self.recommended_action() + "\n"
  if self.findings.length() == 0 {
    output + "\nNo findings."
  } else {
    output = output + "\nFindings:\n"
    for finding in self.findings {
      output = output +
        "- [" +
        finding.severity().label() +
        "] " +
        finding.kind().label()
      if finding.key() != "" {
        output = output + " key=" + finding.key()
      }
      output = output +
        " offset=" +
        finding.offset().to_string() +
        ": " +
        finding.message() +
        "\n"
    }
    output
  }
}

///|
pub fn AuditReport::json_report(self : AuditReport) -> String {
  let mut output = "{"
  output = output + "\"fields\":" + self.parsed.field_count().to_string() + ","
  output = output + "\"findings\":" + self.finding_count().to_string() + ","
  output = output + "\"risk_level\":\"" + escape_json(self.risk_level()) + "\","
  output = output + "\"risk_score\":" + self.risk_score().to_string() + ","
  output = output +
    "\"recommended_action\":\"" +
    escape_json(self.recommended_action()) +
    "\","
  output = output + "\"items\":["
  for index = 0; index < self.findings.length(); index = index + 1 {
    let finding = self.findings[index]
    if index > 0 {
      output = output + ","
    }
    output = output + "{"
    output = output +
      "\"kind\":\"" +
      escape_json(finding.kind().label()) +
      "\","
    output = output +
      "\"severity\":\"" +
      escape_json(finding.severity().label()) +
      "\","
    output = output + "\"key\":\"" + escape_json(finding.key()) + "\","
    output = output + "\"offset\":" + finding.offset().to_string() + ","
    output = output + "\"message\":\"" + escape_json(finding.message()) + "\""
    output = output + "}"
  }
  output + "]}"
}

///|
pub fn parse(line : String) -> ParseResult {
  let chars = line.to_array()
  let fields : Array[Field] = []
  let errors : Array[ParseError] = []
  let mut index = 0
  while index < chars.length() {
    while index < chars.length() && is_space(chars[index]) {
      index = index + 1
    }
    if index >= chars.length() {
      break
    }
    if chars[index] == '=' {
      errors.push({
        kind: UnexpectedEquals,
        message: "field cannot start with `=`",
        offset: index,
      })
      index = skip_token(chars, index + 1)
      continue
    }
    let key_start = index
    while index < chars.length() &&
          !is_space(chars[index]) &&
          chars[index] != '=' {
      index = index + 1
    }
    let key = String::from_array(chars[key_start:index])
    if key == "" {
      errors.push({
        kind: EmptyKey,
        message: "field key is empty",
        offset: key_start,
      })
      index = index + 1
      continue
    }
    if !valid_key(key) {
      errors.push({
        kind: InvalidKey,
        message: "invalid key `" + key + "`",
        offset: key_start,
      })
    }
    if index < chars.length() && chars[index] == '=' {
      index = index + 1
      if index < chars.length() && chars[index] == '"' {
        let value_start = index
        let step = parse_quoted(chars, index + 1)
        fields.push({
          key,
          value: step.value,
          quoted: true,
          flag: false,
          offset: key_start,
        })
        index = step.index
        if !step.ok {
          errors.push({
            kind: UnterminatedQuote,
            message: "quoted value is missing closing quote",
            offset: value_start,
          })
        }
      } else {
        let value_start = index
        while index < chars.length() && !is_space(chars[index]) {
          if chars[index] == '"' {
            errors.push({
              kind: BareQuote,
              message: "quote inside bare value should be escaped or quoted",
              offset: index,
            })
          }
          index = index + 1
        }
        fields.push({
          key,
          value: String::from_array(chars[value_start:index]),
          quoted: false,
          flag: false,
          offset: key_start,
        })
      }
    } else {
      fields.push({
        key,
        value: "true",
        quoted: false,
        flag: true,
        offset: key_start,
      })
    }
  }
  { source: line, fields, errors }
}

///|
pub fn is_valid(line : String) -> Bool {
  parse(line).is_valid()
}

///|
pub fn audit_line(line : String) -> AuditReport {
  audit_line_with_policy(line, AuditPolicy::default())
}

///|
pub fn audit_line_with_policy(
  line : String,
  policy : AuditPolicy,
) -> AuditReport {
  audit(parse(line), policy)
}

///|
pub fn audit(parsed : ParseResult, policy : AuditPolicy) -> AuditReport {
  let findings : Array[Finding] = []
  for err in parsed.errors() {
    findings.push(
      Finding::new(
        SyntaxError,
        Critical,
        "",
        err.message(),
        offset=err.offset(),
      ),
    )
  }
  if parsed.field_count() > policy.max_fields {
    findings.push(
      Finding::new(
        TooManyFields,
        Warning,
        "",
        "line has " +
        parsed.field_count().to_string() +
        " fields; configured limit is " +
        policy.max_fields.to_string(),
      ),
    )
  }
  for key in policy.required_keys {
    if !parsed.has_key(key) {
      findings.push(
        Finding::new(
          RequiredKeyMissing,
          Critical,
          key,
          "required key `" + key + "` is missing",
        ),
      )
    }
  }
  for index = 0; index < parsed.fields.length(); index = index + 1 {
    let field = parsed.fields[index]
    if policy.flag_duplicate_keys {
      for prev = 0; prev < index; prev = prev + 1 {
        if parsed.fields[prev].key() == field.key() {
          findings.push(
            Finding::new(
              DuplicateKey,
              Warning,
              field.key(),
              "key appears more than once; later values may hide earlier ones",
              offset=field.offset(),
            ),
          )
        }
      }
    }
    if field.value().to_array().length() > policy.max_value_length {
      findings.push(
        Finding::new(
          ValueTooLong,
          Warning,
          field.key(),
          "value length exceeds configured limit",
          offset=field.offset(),
        ),
      )
    }
    if policy.flag_blank_values && !field.is_flag() && field.value() == "" {
      findings.push(
        Finding::new(
          BlankValue,
          Warning,
          field.key(),
          "explicit blank value may be hard to distinguish from missing data",
          offset=field.offset(),
        ),
      )
    }
    if policy.flag_flag_fields && field.is_flag() {
      findings.push(
        Finding::new(
          FlagField,
          Info,
          field.key(),
          "flag field has no explicit value",
          offset=field.offset(),
        ),
      )
    }
    if has_control(field.value()) {
      findings.push(
        Finding::new(
          ControlCharacter,
          Critical,
          field.key(),
          "field value contains a control character",
          offset=field.offset(),
        ),
      )
    }
  }
  { parsed, findings }
}

///|
priv struct QuotedStep {
  ok : Bool
  index : Int
  value : String
}

///|
fn parse_quoted(chars : Array[Char], start : Int) -> QuotedStep {
  let output : Array[Char] = []
  let mut index = start
  while index < chars.length() {
    let char = chars[index]
    if char == '"' {
      return { ok: true, index: index + 1, value: String::from_array(output) }
    } else if char == '\\' && index + 1 < chars.length() {
      let next = chars[index + 1]
      if next == 'n' {
        output.push('\n')
      } else if next == 't' {
        output.push('\t')
      } else if next == 'r' {
        output.push('\r')
      } else {
        output.push(next)
      }
      index = index + 2
    } else {
      output.push(char)
      index = index + 1
    }
  }
  { ok: false, index, value: String::from_array(output) }
}

///|
fn skip_token(chars : Array[Char], start : Int) -> Int {
  let mut index = start
  while index < chars.length() && !is_space(chars[index]) {
    index = index + 1
  }
  index
}

///|
fn valid_key(key : String) -> Bool {
  let chars = key.to_array()
  if chars.length() == 0 {
    return false
  }
  for char in chars {
    if !is_key_char(char) {
      return false
    }
  }
  true
}

///|
fn is_key_char(char : Char) -> Bool {
  is_ascii_alnum(char) || char == '_' || char == '-' || char == '.'
}

///|
fn is_ascii_alnum(char : Char) -> Bool {
  (char >= 'a' && char <= 'z') ||
  (char >= 'A' && char <= 'Z') ||
  (char >= '0' && char <= '9')
}

///|
fn is_space(char : Char) -> Bool {
  char == ' ' || char == '\t'
}

///|
fn needs_quote(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() == 0 {
    return true
  }
  for char in chars {
    if is_space(char) ||
      char == '"' ||
      char == '=' ||
      char == '\n' ||
      char == '\r' {
      return true
    }
  }
  false
}

///|
fn has_control(value : String) -> Bool {
  for char in value.to_array() {
    if char < ' ' && char != '\t' {
      return true
    }
  }
  false
}

///|
fn escape_logfmt(input : String) -> String {
  let output : Array[Char] = []
  for char in input.to_array() {
    if char == '"' {
      output.push('\\')
      output.push('"')
    } else if char == '\\' {
      output.push('\\')
      output.push('\\')
    } else if char == '\n' {
      output.push('\\')
      output.push('n')
    } else if char == '\r' {
      output.push('\\')
      output.push('r')
    } else if char == '\t' {
      output.push('\\')
      output.push('t')
    } else {
      output.push(char)
    }
  }
  String::from_array(output)
}

///|
fn escape_json(input : String) -> String {
  let output : Array[Char] = []
  for char in input.to_array() {
    if char == '"' {
      output.push('\\')
      output.push('"')
    } else if char == '\\' {
      output.push('\\')
      output.push('\\')
    } else if char == '\n' {
      output.push('\\')
      output.push('n')
    } else if char == '\r' {
      output.push('\\')
      output.push('r')
    } else if char == '\t' {
      output.push('\\')
      output.push('t')
    } else {
      output.push(char)
    }
  }
  String::from_array(output)
}