///|
/// A compact data-retention manifest parsed by CapsuleTrace.
///
/// Records:
///
/// - `day: `
/// - `rule: id | purpose | data-kind | max-days | anonymize-after | consent/no-consent | regions | internal-only/external-ok | owner | note`
/// - `capsule: id | subject | purpose | data-kind | collected-day | last-used-day | consent | sensitivity | region | encrypted/plain | sharing | note`
pub(all) enum RetentionRecordKind {
  DayRecord
  RuleRecord
  CapsuleRecord
  UnknownRetentionRecord
} derive(Eq, Debug)

///|
pub(all) struct RetentionParseIssue {
  line : Int
  record : String
  message : String
} derive(Eq, Debug)

///|
pub(all) struct RetentionManifest {
  today : Int
  rules : Array[RetentionRule]
  capsules : Array[DataCapsule]
  issues : Array[RetentionParseIssue]
} derive(Eq, Debug)

///|
pub fn RetentionRecordKind::label(self : RetentionRecordKind) -> String {
  match self {
    DayRecord => "day"
    RuleRecord => "rule"
    CapsuleRecord => "capsule"
    UnknownRetentionRecord => "unknown"
  }
}

///|
pub fn parse_retention_manifest(raw : StringView) -> RetentionManifest {
  let mut today = 0
  let rules : Array[RetentionRule] = []
  let capsules : Array[DataCapsule] = []
  let issues : Array[RetentionParseIssue] = []
  for line_number, raw_line in raw.split("\n").iter2() {
    let line = raw_line.trim()
    if !line.is_empty() && !line.has_prefix("#") {
      match line.split_once(":") {
        Some((record_view, value_view)) => {
          let record = record_view.trim().to_lower().to_owned()
          let value = value_view.trim()
          match record {
            "day" =>
              match parse_positive_int(value) {
                Some(parsed) => today = parsed
                None =>
                  issues.push(
                    parse_issue(
                      line_number + 1,
                      "day",
                      "day must be a non-negative integer",
                    ),
                  )
              }
            "rule" =>
              match parse_rule_record(value, line_number + 1, issues) {
                Some(rule) => rules.push(rule)
                None => ()
              }
            "capsule" =>
              match parse_capsule_record(value, line_number + 1, issues) {
                Some(capsule) => capsules.push(capsule)
                None => ()
              }
            _ =>
              issues.push(
                parse_issue(
                  line_number + 1,
                  record,
                  "record must be day, rule, capsule, or comment",
                ),
              )
          }
        }
        None =>
          issues.push(
            parse_issue(
              line_number + 1,
              "",
              "manifest line must use ': '",
            ),
          )
      }
    }
  }
  { today, rules, capsules, issues }
}

///|
pub fn plan_manifest(raw : StringView) -> RetentionPlan {
  let manifest = parse_retention_manifest(raw)
  plan_retention(manifest.capsules, manifest.rules, manifest.today)
}

///|
pub fn RetentionManifest::is_parseable(self : RetentionManifest) -> Bool {
  self.today > 0 && self.issues.is_empty()
}

///|
pub fn RetentionManifest::to_markdown(self : RetentionManifest) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Manifest Parse")
  lines.push("")
  lines.push("- Day: " + self.today.to_string())
  lines.push("- Rules: " + self.rules.length().to_string())
  lines.push("- Capsules: " + self.capsules.length().to_string())
  lines.push("- Issues: " + self.issues.length().to_string())
  lines.push("")
  lines.push("| Line | Record | Message |")
  lines.push("| --- | --- | --- |")
  if self.issues.is_empty() {
    lines.push("| - | info | no parse issues |")
  } else {
    for issue in self.issues {
      lines.push(
        "| " +
        issue.line.to_string() +
        " | " +
        escape_cell(issue.record) +
        " | " +
        escape_cell(issue.message) +
        " |",
      )
    }
  }
  lines.join("\n")
}

///|
pub fn sample_retention_manifest() -> String {
  [
    "# CapsuleTrace data-retention manifest", "day: 220", "rule: analytics.behavior | analytics | behavior | 180 | 30 | consent | eu,cn,us | internal-only | privacy-analytics | Aggregate visitor behavior quickly.",
    "rule: support.ticket | support | ticket | 365 | 120 | no-consent | cn,sg | external-ok | support-ops | Support vendors are allowed processors.",
    "rule: billing.invoice | billing | invoice | 2555 | 2555 | no-consent | cn | internal-only | finance | Billing records stay local.",
    "capsule: cap.active-analytics | visitor | analytics | behavior | 200 | 216 | granted | sensitive | cn | encrypted | internal | Active telemetry.",
    "capsule: cap.idle-analytics | visitor | analytics | behavior | 120 | 150 | granted | sensitive | cn | encrypted | internal | Idle telemetry.",
    "capsule: cap.withdrawn | visitor | analytics | behavior | 160 | 190 | withdrawn | sensitive | cn | encrypted | internal | Withdrawn consent.",
    "capsule: cap.unencrypted-ticket | customer | support | ticket | 40 | 205 | not-required | confidential | sg | plain | processor | Missing encryption.",
    "capsule: cap.region-mismatch | buyer | billing | invoice | 20 | 210 | not-required | restricted | us | encrypted | internal | Invoice outside region.",
  ].join("\n")
}

///|
fn parse_rule_record(
  value : StringView,
  line : Int,
  issues : Array[RetentionParseIssue],
) -> RetentionRule? {
  let fields = split_fields(value)
  if fields.length() < 10 {
    issues.push(
      parse_issue(
        line, "rule", "expected 10 fields: id | purpose | data-kind | max-days | anonymize-after | consent/no-consent | regions | sharing | owner | note",
      ),
    )
    None
  } else {
    match
      (
        parse_positive_int(fields[3]),
        parse_positive_int(fields[4]),
        parse_consent_required(fields[5]),
        parse_rule_sharing(fields[7]),
      ) {
      (
        Some(max_keep_days),
        Some(anonymize_after_days),
        Some(requires_consent),
        Some(allow_external_sharing),
      ) =>
        Some(
          retention_rule(
            fields[0],
            fields[1],
            fields[2],
            max_keep_days,
            anonymize_after_days,
            requires_consent,
            split_csv(fields[6]),
            allow_external_sharing,
            fields[8],
            fields[9],
          ),
        )
      (None, _, _, _) => {
        issues.push(
          parse_issue(line, "rule", "max-days must be a non-negative integer"),
        )
        None
      }
      (_, None, _, _) => {
        issues.push(
          parse_issue(
            line, "rule", "anonymize-after must be a non-negative integer",
          ),
        )
        None
      }
      (_, _, None, _) => {
        issues.push(
          parse_issue(
            line, "rule", "consent field must be consent or no-consent",
          ),
        )
        None
      }
      (_, _, _, None) => {
        issues.push(
          parse_issue(
            line, "rule", "sharing field must be internal-only or external-ok",
          ),
        )
        None
      }
    }
  }
}

///|
fn parse_capsule_record(
  value : StringView,
  line : Int,
  issues : Array[RetentionParseIssue],
) -> DataCapsule? {
  let fields = split_fields(value)
  if fields.length() < 12 {
    issues.push(
      parse_issue(
        line, "capsule", "expected 12 fields: id | subject | purpose | data-kind | collected | last-used | consent | sensitivity | region | encrypted/plain | sharing | note",
      ),
    )
    None
  } else {
    match
      (
        parse_positive_int(fields[4]),
        parse_positive_int(fields[5]),
        parse_consent_state(fields[6]),
        parse_sensitivity(fields[7]),
        parse_encryption(fields[9]),
        parse_sharing_mode(fields[10]),
      ) {
      (
        Some(collected_day),
        Some(last_used_day),
        Some(consent),
        Some(sensitivity),
        Some(encrypted),
        Some(sharing),
      ) =>
        Some(
          data_capsule(
            fields[0],
            fields[1],
            fields[2],
            fields[3],
            collected_day,
            last_used_day,
            consent,
            sensitivity,
            fields[8],
            encrypted,
            sharing,
            fields[11],
          ),
        )
      (None, _, _, _, _, _) => {
        issues.push(
          parse_issue(
            line, "capsule", "collected day must be a non-negative integer",
          ),
        )
        None
      }
      (_, None, _, _, _, _) => {
        issues.push(
          parse_issue(
            line, "capsule", "last-used day must be a non-negative integer",
          ),
        )
        None
      }
      (_, _, None, _, _, _) => {
        issues.push(
          parse_issue(
            line, "capsule", "consent must be granted, denied, expired, withdrawn, or not-required",
          ),
        )
        None
      }
      (_, _, _, None, _, _) => {
        issues.push(
          parse_issue(
            line, "capsule", "sensitivity must be public, internal, confidential, sensitive, or restricted",
          ),
        )
        None
      }
      (_, _, _, _, None, _) => {
        issues.push(
          parse_issue(line, "capsule", "encryption must be encrypted or plain"),
        )
        None
      }
      (_, _, _, _, _, None) => {
        issues.push(
          parse_issue(
            line, "capsule", "sharing must be internal, processor, or public",
          ),
        )
        None
      }
    }
  }
}

///|
fn parse_positive_int(value : StringView) -> Int? {
  let text = value.trim().to_owned()
  if text.is_empty() {
    None
  } else {
    let mut result = 0
    for index in 0.. 57 {
        return None
      }
      result = result * 10 + (code - 48)
    }
    Some(result)
  }
}

///|
fn parse_consent_required(value : String) -> Bool? {
  match normalized(value) {
    "consent" => Some(true)
    "requires-consent" => Some(true)
    "no-consent" => Some(false)
    "not-required" => Some(false)
    _ => None
  }
}

///|
fn parse_rule_sharing(value : String) -> Bool? {
  match normalized(value) {
    "internal-only" => Some(false)
    "external-ok" => Some(true)
    "processors-ok" => Some(true)
    _ => None
  }
}

///|
fn parse_consent_state(value : String) -> ConsentState? {
  match normalized(value) {
    "granted" => Some(ConsentGranted)
    "denied" => Some(ConsentDenied)
    "expired" => Some(ConsentExpired)
    "withdrawn" => Some(ConsentWithdrawn)
    "not-required" => Some(ConsentNotRequired)
    _ => None
  }
}

///|
fn parse_sensitivity(value : String) -> CapsuleSensitivity? {
  match normalized(value) {
    "public" => Some(PublicData)
    "internal" => Some(InternalData)
    "confidential" => Some(ConfidentialData)
    "sensitive" => Some(SensitiveData)
    "restricted" => Some(RestrictedData)
    _ => None
  }
}

///|
fn parse_encryption(value : String) -> Bool? {
  match normalized(value) {
    "encrypted" => Some(true)
    "plain" => Some(false)
    "unencrypted" => Some(false)
    _ => None
  }
}

///|
fn parse_sharing_mode(value : String) -> SharingMode? {
  match normalized(value) {
    "internal" => Some(InternalOnly)
    "processor" => Some(ProcessorShared)
    "processors" => Some(ProcessorShared)
    "public" => Some(PublicShared)
    _ => None
  }
}

///|
fn split_fields(value : StringView) -> Array[String] {
  let fields : Array[String] = []
  for field in value.split("|") {
    fields.push(field.trim().to_owned())
  }
  fields
}

///|
fn split_csv(value : String) -> Array[String] {
  let items : Array[String] = []
  for raw_item in value.split(",") {
    let item = raw_item.trim()
    if !item.is_empty() {
      items.push(item.to_owned())
    }
  }
  items
}

///|
fn normalized(value : String) -> String {
  value.trim().to_lower().to_owned()
}

///|
fn parse_issue(
  line : Int,
  record : String,
  message : String,
) -> RetentionParseIssue {
  { line, record, message }
}

///|
fn escape_cell(value : String) -> String {
  value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}