///|
pub enum PipelineStage {
  Normalize
  Detect
  Resolve
  Apply
  Audit
  Complete
} derive(Debug, Eq)

///|
pub(all) struct PipelineConfig {
  policy : RedactionPolicy
  rules : Array[Rule]
  normalize_input : Bool
  include_keyword_diagnostics : Bool
  keep_review_findings : Bool
} derive(Debug, Eq)

///|
pub(all) struct PipelineTrace {
  stage : PipelineStage
  message : String
} derive(Debug, Eq)

///|
pub(all) struct PipelineResult {
  original : String
  normalized : String
  findings : Array[Finding]
  result : DeidResult
  statistics : ScanStatistics
  diagnostics : Array[Diagnostic]
  trace : Array[PipelineTrace]
} derive(Debug)

///|
pub fn PipelineConfig::default() -> PipelineConfig {
  {
    policy: RedactionPolicy::default(),
    rules: comprehensive_rules(),
    normalize_input: true,
    include_keyword_diagnostics: true,
    keep_review_findings: true,
  }
}

///|
pub fn PipelineResult::text(self : PipelineResult) -> String {
  self.result.text
}

///|
pub fn PipelineResult::audit(self : PipelineResult) -> AuditReport {
  self.result.audit
}

///|
pub fn pipeline_stage_name(stage : PipelineStage) -> String {
  match stage {
    Normalize => "normalize"
    Detect => "detect"
    Resolve => "resolve"
    Apply => "apply"
    Audit => "audit"
    Complete => "complete"
  }
}

///|
pub fn pipeline_trace_text(trace : Array[PipelineTrace]) -> String {
  trace
  .map(fn(item) { "[\{pipeline_stage_name(item.stage)}] \{item.message}" })
  .join("\n")
}

///|
fn pipeline_input(input : String, config : PipelineConfig) -> String {
  if config.normalize_input {
    sanitize_input(input)
  } else {
    input
  }
}

///|
fn pipeline_review_diagnostics(
  input : String,
  findings : Array[Finding],
  config : PipelineConfig,
) -> Array[Diagnostic] {
  let diagnostics = diagnose_findings(input, findings)
  if config.include_keyword_diagnostics && has_sensitive_keyword(input) {
    diagnostics.push({
      code: "KEYWORD_CONTEXT",
      severity: Info,
      message: "sensitive field labels were found; verify contextual recall",
      start: 0,
      end: input.length(),
    })
  }
  diagnostics
}

///|
pub fn run_pipeline(
  input : String,
  config : PipelineConfig,
) -> PipelineResult raise DeidError {
  let trace = [{ stage: Normalize, message: "input normalization selected" }]
  let normalized = pipeline_input(input, config)
  trace.push({
    stage: Detect,
    message: "scanning \{config.rules.length()} rules",
  })
  let candidates = scan(normalized, rules=config.rules)
  trace.push({
    stage: Resolve,
    message: "resolved \{candidates.length()} findings",
  })
  let selected = policy_filter_findings(candidates, config.policy)
  let result = redact_with_config(
    normalized,
    config_to_pipeline(config),
    rules=config.rules,
  )
  trace.push({
    stage: Apply,
    message: "applied \{result.offsets.length()} replacements",
  })
  let diagnostics = pipeline_review_diagnostics(normalized, selected, config)
  trace.push({ stage: Audit, message: "audit generated" })
  trace.push({ stage: Complete, message: "pipeline complete" })
  {
    original: input,
    normalized,
    findings: if config.keep_review_findings {
      selected
    } else {
      result.findings
    },
    result,
    statistics: scan_statistics(normalized, config.rules, selected),
    diagnostics,
    trace,
  }
}

///|
fn config_to_pipeline(config : PipelineConfig) -> RedactionConfig {
  { ..RedactionConfig::default(), policy: config.policy }
}

///|
pub fn pipeline_with_policy(
  input : String,
  policy : RedactionPolicy,
) -> PipelineResult raise DeidError {
  run_pipeline(input, { ..PipelineConfig::default(), policy, })
}

///|
pub fn pipeline_findings(result : PipelineResult) -> Array[Finding] {
  result.findings
}

///|
pub fn pipeline_has_errors(result : PipelineResult) -> Bool {
  diagnostics_have_errors(result.diagnostics)
}

///|
pub fn pipeline_is_safe(result : PipelineResult) -> Bool {
  !pipeline_has_errors(result) &&
  output_contract_passes(result.normalized, result.result)
}

///|
pub fn pipeline_summary(result : PipelineResult) -> String {
  [
    "normalized_length=\{result.normalized.length()}",
    "findings=\{result.findings.length()}",
    "applied=\{result.result.offsets.length()}",
    "diagnostics=\{result.diagnostics.length()}",
    "safe=\{pipeline_is_safe(result)}",
  ].join("\n")
}

///|
pub fn pipeline_to_json(result : PipelineResult) -> String {
  "{" +
  "\"original_length\":\{result.original.length()}," +
  "\"normalized_length\":\{result.normalized.length()}," +
  "\"result\":\{result_to_json(result.result)}," +
  "\"statistics\":{" +
  "\"rule_count\":\{result.statistics.rule_count}," +
  "\"candidate_count\":\{result.statistics.candidate_count}," +
  "\"critical_count\":\{result.statistics.critical_count}" +
  "}," +
  "\"diagnostics\":\{diagnostics_to_json(result.diagnostics)}" +
  "}"
}

///|
pub fn pipeline_for_profile(
  input : String,
  profile : Profile,
  mode : ReplacementMode,
) -> PipelineResult raise DeidError {
  let pack = select_pack(profile)
  let policy = with_policy_mode(RedactionPolicy::default(), mode)
  run_pipeline(input, { ..PipelineConfig::default(), policy, rules: pack.rules })
}

///|
pub fn pipeline_replay(
  input : String,
  result : PipelineResult,
) -> Bool raise DeidError {
  let replay = run_pipeline(input, {
    ..PipelineConfig::default(),
    rules: comprehensive_rules(),
  })
  replay.result.text == result.result.text
}

///|
pub fn pipeline_trace_checksum(result : PipelineResult) -> String {
  stable_hash(pipeline_trace_text(result.trace))
}