///|
/// A deterministic processing operation available in the production pipeline.
pub enum ProcessingStage {
  RemoveMeanStage
  RemoveTrendStage
  CosineTaperStage
  MedianDenoiseStage
  PeakNormalizeStage
  ZScoreNormalizeStage
  IntegrateStage
  DifferentiateStage
  EnvelopeStage
  ClipStage
}

///|
/// A configured stage with one numeric parameter.
pub struct ConfiguredStage {
  stage : ProcessingStage
  parameter : Double
  enabled : Bool
}

///|
/// One record in a trace transformation history.
pub struct ProcessingHistoryEntry {
  sequence : Int
  name : String
  input_samples : Int
  output_samples : Int
  input_rms : Double
  output_rms : Double
}

///|
/// Output of a processing graph, including auditable history.
pub struct ProcessingGraphResult {
  trace : Trace
  history : Array[ProcessingHistoryEntry]
  successful_stages : Int
  rejected_stages : Int
  final_score : Double
}

///|
/// A processing plan used by batch waveform workflows.
pub struct ProcessingPlan {
  name : String
  stages : Array[ConfiguredStage]
  reject_on_invalid : Bool
  expected_sample_rate : Double
}

///|
/// Return all stage constructors for configuration validation.
pub fn supported_processing_stages() -> Array[ProcessingStage] {
  [
    RemoveMeanStage,
    RemoveTrendStage,
    CosineTaperStage,
    MedianDenoiseStage,
    PeakNormalizeStage,
    ZScoreNormalizeStage,
    IntegrateStage,
    DifferentiateStage,
    EnvelopeStage,
    ClipStage,
  ]
}

///|
/// Construct a default production processing plan.
pub fn default_processing_plan() -> ProcessingPlan {
  {
    name: "event-preprocess",
    stages: [
      { stage: RemoveMeanStage, parameter: 0.0, enabled: true },
      { stage: RemoveTrendStage, parameter: 0.0, enabled: true },
      { stage: CosineTaperStage, parameter: 0.05, enabled: true },
    ],
    reject_on_invalid: true,
    expected_sample_rate: 0.0,
  }
}

///|
/// Return a stable stage name for reports and tracing.
pub fn processing_stage_name(stage : ProcessingStage) -> String {
  match stage {
    RemoveMeanStage => "remove-mean"
    RemoveTrendStage => "remove-trend"
    CosineTaperStage => "cosine-taper"
    MedianDenoiseStage => "median-denoise"
    PeakNormalizeStage => "peak-normalize"
    ZScoreNormalizeStage => "zscore-normalize"
    IntegrateStage => "integrate"
    DifferentiateStage => "differentiate"
    EnvelopeStage => "envelope"
    ClipStage => "clip"
  }
}

///|
/// Apply one configured operation to a trace copy.
pub fn apply_processing_stage(
  trace : Trace,
  configured : ConfiguredStage,
) -> Result[Trace, String] {
  if !configured.enabled {
    return Ok(trace.clone())
  }
  match configured.stage {
    RemoveMeanStage => {
      let result = trace.clone()
      result.demean()
      Ok(result)
    }
    RemoveTrendStage => {
      let result = trace_with_data(trace, detrend_copy(trace.data))
      Ok(result)
    }
    CosineTaperStage => {
      let result = trace.clone()
      cosine_taper(result.data, configured.parameter)
      Ok(result)
    }
    MedianDenoiseStage => {
      let radius = configured.parameter.to_int().max(0)
      Ok(trace_with_data(trace, moving_median(trace.data, radius)))
    }
    PeakNormalizeStage => Ok(normalize_trace_peak(trace))
    ZScoreNormalizeStage => Ok(normalize_trace_zscore(trace))
    IntegrateStage => Ok(integrate_trace(trace))
    DifferentiateStage => Ok(differentiate_trace(trace))
    EnvelopeStage => Ok(envelope_trace(trace))
    ClipStage => {
      let limit = configured.parameter.abs()
      if limit <= 0.0 {
        Err("clip stage requires a positive limit")
      } else {
        Ok(clamp_trace(trace, -limit, limit))
      }
    }
  }
}

///|
/// Validate plan metadata before a batch run.
pub fn validate_processing_plan(plan : ProcessingPlan) -> Result[Unit, String] {
  if plan.name == "" {
    return Err("Processing plan name cannot be empty")
  }
  if plan.expected_sample_rate < 0.0 {
    return Err("Expected sample rate cannot be negative")
  }
  for configured in plan.stages {
    if configured.stage is CosineTaperStage &&
      (configured.parameter < 0.0 || configured.parameter > 0.5) {
      return Err("Cosine taper fraction must be between zero and one half")
    }
    if configured.stage is ClipStage &&
      configured.enabled &&
      configured.parameter <= 0.0 {
      return Err("Clip stage needs a positive limit")
    }
  }
  Ok(())
}

///|
/// Compute a quality score from a processing result.
pub fn processing_result_score(
  input : Trace,
  output : Trace,
  rejected_stages : Int,
) -> Double {
  let validity = if trace_is_well_formed(output) { 1.0 } else { 0.0 }
  let size_score = if input.npts() == 0 {
    0.0
  } else {
    (output.npts().to_double() / input.npts().to_double()).min(1.0)
  }
  let rejection_score = (1.0 - rejected_stages.to_double() * 0.1).max(0.0)
  (0.5 * validity + 0.3 * size_score + 0.2 * rejection_score).min(1.0)
}

///|
/// Execute a plan while recording input and output statistics for every stage.
pub fn run_processing_plan(
  trace : Trace,
  plan : ProcessingPlan,
) -> ProcessingGraphResult {
  let history : Array[ProcessingHistoryEntry] = Array::new()
  let mut current = trace.clone()
  let mut successful = 0
  let mut rejected = 0
  for index = 0; index < plan.stages.length(); index = index + 1 {
    let configured = plan.stages[index]
    if !configured.enabled {
      continue
    }
    let before = current
    match apply_processing_stage(current, configured) {
      Ok(output) => {
        current = output
        successful += 1
        history.push({
          sequence: index,
          name: processing_stage_name(configured.stage),
          input_samples: before.npts(),
          output_samples: current.npts(),
          input_rms: rms_samples(before.data),
          output_rms: rms_samples(current.data),
        })
      }
      Err(_) => {
        rejected += 1
        if plan.reject_on_invalid {
          break
        }
      }
    }
  }
  {
    trace: current,
    history,
    successful_stages: successful,
    rejected_stages: rejected,
    final_score: processing_result_score(trace, current, rejected),
  }
}

///|
/// Append a stage to a plan while preserving value semantics.
pub fn append_processing_stage(
  plan : ProcessingPlan,
  stage : ProcessingStage,
  parameter : Double,
) -> ProcessingPlan {
  let stages = plan.stages.copy()
  stages.push({ stage, parameter, enabled: true })
  {
    name: plan.name,
    stages,
    reject_on_invalid: plan.reject_on_invalid,
    expected_sample_rate: plan.expected_sample_rate,
  }
}

///|
/// Enable or disable a stage by sequence index.
pub fn set_processing_stage_enabled(
  plan : ProcessingPlan,
  sequence : Int,
  enabled : Bool,
) -> ProcessingPlan {
  let stages : Array[ConfiguredStage] = Array::new()
  for index = 0; index < plan.stages.length(); index = index + 1 {
    let current = plan.stages[index]
    stages.push(
      if index == sequence {
        { stage: current.stage, parameter: current.parameter, enabled }
      } else {
        current
      },
    )
  }
  {
    name: plan.name,
    stages,
    reject_on_invalid: plan.reject_on_invalid,
    expected_sample_rate: plan.expected_sample_rate,
  }
}

///|
/// Serialize processing history for a run artifact.
pub fn processing_history_to_csv(
  history : Array[ProcessingHistoryEntry],
) -> String {
  let mut output = "sequence,stage,input_samples,output_samples,input_rms,output_rms\n"
  for entry in history {
    output = output +
      entry.sequence.to_string() +
      "," +
      entry.name +
      "," +
      entry.input_samples.to_string() +
      "," +
      entry.output_samples.to_string() +
      "," +
      entry.input_rms.to_string() +
      "," +
      entry.output_rms.to_string() +
      "\n"
  }
  output
}

///|
/// Return the number of enabled operations in a plan.
pub fn enabled_stage_count(plan : ProcessingPlan) -> Int {
  plan.stages.filter(stage => stage.enabled).length()
}

///|
/// Return a compact plan description.
pub fn processing_plan_text(plan : ProcessingPlan) -> String {
  let names = plan.stages
    .filter(stage => stage.enabled)
    .map(stage => processing_stage_name(stage.stage))
  let mut joined = ""
  for index = 0; index < names.length(); index = index + 1 {
    if index > 0 {
      joined = joined + " -> "
    }
    joined = joined + names[index]
  }
  "name=" +
  plan.name +
  " stages=" +
  joined +
  " reject_on_invalid=" +
  plan.reject_on_invalid.to_string()
}

///|
/// Format a graph result headline.
pub fn processing_graph_result_text(result : ProcessingGraphResult) -> String {
  "samples=" +
  result.trace.npts().to_string() +
  " successful=" +
  result.successful_stages.to_string() +
  " rejected=" +
  result.rejected_stages.to_string() +
  " history=" +
  result.history.length().to_string() +
  " score=" +
  result.final_score.to_string()
}