///|
/// Safe export helpers for audit and review systems. Export records contain
/// finding metadata and replacement values, but never emit the original source
/// text unless a caller explicitly selects an authorized raw format.
pub(all) enum AuditExportFormat {
  AuditExportJson
  AuditExportCsv
  AuditExportNdjson
  AuditExportSummary
} derive(Debug, Eq)

///|
pub(all) enum AuditExportRedaction {
  AuditExportMasked
  AuditExportTokenOnly
  AuditExportMetadataOnly
} derive(Debug, Eq)

///|
pub(all) struct AuditExportRequest {
  export_id : String
  document_id : String
  requested_by : String
  format : AuditExportFormat
  redaction : AuditExportRedaction
  query : FindingQuery
  purpose : String
  requested_at : String
  max_records : Int
} derive(Debug)

///|
pub(all) struct AuditExportRecord {
  finding_id : String
  document_id : String
  kind : String
  rule_id : String
  label : String
  start : Int
  end : Int
  confidence : Int
  risk : String
  replacement : String
  source_checksum : String
} derive(Debug, Eq)

///|
pub(all) struct AuditExport {
  export_id : String
  document_id : String
  format : AuditExportFormat
  redaction : AuditExportRedaction
  records : Array[AuditExportRecord]
  total_matches : Int
  truncated : Bool
  generated_at : String
  checksum : String
} derive(Debug)

///|
pub fn audit_export_format_name(format : AuditExportFormat) -> String {
  match format {
    AuditExportJson => "json"
    AuditExportCsv => "csv"
    AuditExportNdjson => "ndjson"
    AuditExportSummary => "summary"
  }
}

///|
pub fn audit_export_redaction_name(redaction : AuditExportRedaction) -> String {
  match redaction {
    AuditExportMasked => "masked"
    AuditExportTokenOnly => "token-only"
    AuditExportMetadataOnly => "metadata-only"
  }
}

///|
pub fn audit_export_request(
  export_id : String,
  document_id : String,
  requested_by : String,
  format : AuditExportFormat,
  redaction : AuditExportRedaction,
  purpose : String,
  requested_at : String,
) -> AuditExportRequest {
  {
    export_id,
    document_id,
    requested_by,
    format,
    redaction,
    query: FindingQuery::default(),
    purpose,
    requested_at,
    max_records: 10000,
  }
}

///|
pub fn AuditExportRequest::with_query(
  request : AuditExportRequest,
  query : FindingQuery,
) -> AuditExportRequest {
  { ..request, query, }
}

///|
pub fn AuditExportRequest::with_limit(
  request : AuditExportRequest,
  max_records : Int,
) -> AuditExportRequest {
  { ..request, max_records: if max_records < 1 { 1 } else { max_records } }
}

///|
pub fn AuditExportRequest::is_valid(self : AuditExportRequest) -> Bool {
  self.export_id.length() > 0 &&
  self.document_id.length() > 0 &&
  self.requested_by.length() > 0 &&
  self.purpose.length() > 0 &&
  self.requested_at.length() > 0 &&
  self.max_records > 0
}

///|
fn audit_export_record(
  document_id : String,
  finding : Finding,
  redaction : AuditExportRedaction,
) -> AuditExportRecord {
  let replacement = match redaction {
    AuditExportMasked => finding.replacement
    AuditExportTokenOnly => "[" + phi_kind_name(finding.kind) + "]"
    AuditExportMetadataOnly => ""
  }
  {
    finding_id: finding.id,
    document_id,
    kind: phi_kind_name(finding.kind),
    rule_id: finding.rule_id,
    label: finding.label,
    start: finding.start,
    end: finding.end,
    confidence: finding.confidence,
    risk: "\{risk_level(finding)}",
    replacement,
    source_checksum: stable_hash(finding.text),
  }
}

///|
pub fn build_audit_export(
  request : AuditExportRequest,
  findings : Array[Finding],
  generated_at : String,
) -> AuditExport {
  let selected = query_findings(findings, request.query)
  let limited = if selected.length() > request.max_records {
    selected[:request.max_records].to_owned()
  } else {
    selected
  }
  let records = limited.map(fn(finding) {
    audit_export_record(request.document_id, finding, request.redaction)
  })
  {
    export_id: request.export_id,
    document_id: request.document_id,
    format: request.format,
    redaction: request.redaction,
    records,
    total_matches: selected.length(),
    truncated: selected.length() > limited.length(),
    generated_at,
    checksum: stable_hash(
      records.map(fn(record) { record.finding_id }).join("\n"),
    ),
  }
}

///|
pub fn AuditExport::is_empty(self : AuditExport) -> Bool {
  self.records.is_empty()
}

///|
pub fn AuditExport::record_count(self : AuditExport) -> Int {
  self.records.length()
}

///|
pub fn AuditExport::metadata_only(self : AuditExport) -> Bool {
  self.redaction == AuditExportMetadataOnly
}

///|
pub fn AuditExport::contains_raw_text(self : AuditExport) -> Bool {
  self.records.any(fn(record) {
    record.replacement.contains(record.source_checksum)
  })
}

///|
fn audit_export_csv_escape(value : String) -> String {
  if value.contains(",") || value.contains("\"") || value.contains("\n") {
    "\"" + value.replace(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
fn audit_export_record_csv(record : AuditExportRecord) -> String {
  [
    record.finding_id,
    record.document_id,
    record.kind,
    record.rule_id,
    record.label,
    record.start.to_string(),
    record.end.to_string(),
    record.confidence.to_string(),
    record.risk,
    record.replacement,
    record.source_checksum,
  ]
  .map(audit_export_csv_escape)
  .join(",")
}

///|
pub fn AuditExport::to_csv(self : AuditExport) -> String {
  let header = "finding_id,document_id,kind,rule_id,label,start,end,confidence,risk,replacement,source_checksum"
  let rows = self.records.map(audit_export_record_csv)
  ([header] + rows).join("\n")
}

///|
fn audit_export_record_json(record : AuditExportRecord) -> String {
  "{" +
  "\"finding_id\":\"" +
  json_escape(record.finding_id) +
  "\"," +
  "\"document_id\":\"" +
  json_escape(record.document_id) +
  "\"," +
  "\"kind\":\"" +
  json_escape(record.kind) +
  "\"," +
  "\"rule_id\":\"" +
  json_escape(record.rule_id) +
  "\"," +
  "\"label\":\"" +
  json_escape(record.label) +
  "\"," +
  "\"start\":" +
  record.start.to_string() +
  "," +
  "\"end\":" +
  record.end.to_string() +
  "," +
  "\"confidence\":" +
  record.confidence.to_string() +
  "," +
  "\"risk\":\"" +
  json_escape(record.risk) +
  "\"," +
  "\"replacement\":\"" +
  json_escape(record.replacement) +
  "\"," +
  "\"source_checksum\":\"" +
  json_escape(record.source_checksum) +
  "\"}"
}

///|
pub fn AuditExport::to_json(self : AuditExport) -> String {
  "{" +
  "\"export_id\":\"" +
  json_escape(self.export_id) +
  "\"," +
  "\"document_id\":\"" +
  json_escape(self.document_id) +
  "\"," +
  "\"format\":\"" +
  audit_export_format_name(self.format) +
  "\"," +
  "\"redaction\":\"" +
  audit_export_redaction_name(self.redaction) +
  "\"," +
  "\"records\":[" +
  self.records.map(audit_export_record_json).join(",") +
  "]," +
  "\"total_matches\":" +
  self.total_matches.to_string() +
  "," +
  "\"truncated\":" +
  self.truncated.to_string() +
  "," +
  "\"generated_at\":\"" +
  json_escape(self.generated_at) +
  "\"," +
  "\"checksum\":\"" +
  json_escape(self.checksum) +
  "\"}"
}

///|
pub fn AuditExport::to_ndjson(self : AuditExport) -> String {
  self.records.map(audit_export_record_json).join("\n")
}

///|
pub fn AuditExport::summary(self : AuditExport) -> String {
  [
    "export_id=" + self.export_id,
    "document_id=" + self.document_id,
    "format=" + audit_export_format_name(self.format),
    "redaction=" + audit_export_redaction_name(self.redaction),
    "records=" + self.records.length().to_string(),
    "total_matches=" + self.total_matches.to_string(),
    "truncated=" + self.truncated.to_string(),
    "generated_at=" + self.generated_at,
    "checksum=" + self.checksum,
  ].join("\n")
}

///|
pub fn AuditExport::render(self : AuditExport) -> String {
  match self.format {
    AuditExportJson => self.to_json()
    AuditExportCsv => self.to_csv()
    AuditExportNdjson => self.to_ndjson()
    AuditExportSummary => self.summary()
  }
}

///|
pub fn audit_export_is_safe(artifact : AuditExport) -> Bool {
  !artifact.contains_raw_text() &&
  artifact.records.all(fn(record) {
    record.source_checksum.length() > 0 && record.finding_id.length() > 0
  })
}

///|
pub fn audit_export_kind_counts(artifact : AuditExport) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for record in artifact.records {
    counts[record.kind] = counts.get_or_default(record.kind, 0) + 1
  }
  counts
}

///|
pub fn audit_export_risk_counts(artifact : AuditExport) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for record in artifact.records {
    counts[record.risk] = counts.get_or_default(record.risk, 0) + 1
  }
  counts
}

///|
pub fn audit_export_checksum(artifact : AuditExport) -> String {
  stable_hash(artifact.to_ndjson())
}

///|
pub fn audit_export_round_trip_stable(artifact : AuditExport) -> Bool {
  artifact.to_json().length() > 0 &&
  audit_export_checksum(artifact).length() > 0
}

///|
pub fn audit_export_limit_is_enforced(
  request : AuditExportRequest,
  findings : Array[Finding],
) -> Bool {
  build_audit_export(request, findings, request.requested_at).records.length() <=
  request.max_records
}