///|
priv struct SweepCandidate {
  dimension : Int
  lag : Int
  stride : Int
  point_count : Int
  finite_h1_count : Int
  longest_finite_h1_lifetime : Double
}

///|
fn sweep_integer_list(
  object : Map[String, Json],
  name : String,
  lower : Int,
  upper : Int,
) -> Array[Int] raise TopologyError {
  let values : Array[Json] = @json.from_json(required_field(object, name)) catch {
    _ => raise TopologyError(name + " must be an array of integers")
  }
  if values.is_empty() || values.length() > 12 {
    raise TopologyError(name + " must contain 1..12 values")
  }
  let result : Array[Int] = []
  for value in values {
    let number = match value {
      Number(n, ..) => n
      _ => raise TopologyError(name + " must contain only integers")
    }
    if !finite(number) ||
      number < lower.to_double() ||
      number > upper.to_double() ||
      number != @math.floor(number) {
      raise TopologyError(name + " contains an out-of-range integer")
    }
    let item = number.to_int()
    if result.contains(item) {
      raise TopologyError(name + " contains a duplicate value")
    }
    result.push(item)
  }
  result
}

///|
/// Evaluate a bounded Cartesian sweep of delay-embedding parameters. Every
/// vector uses the same H1 landscape grid and excludes cutoff-censored bars.
/// The returned CSV tables share the JSON case order.
pub fn series_sweep(
  text : String,
) -> (Json, String, String) raise TopologyError {
  if text.length() > 1000000 {
    raise TopologyError("series sweep request exceeds one million characters")
  }
  let value = @json.parse(text) catch {
    _ => raise TopologyError("invalid series sweep JSON")
  }
  let object = match value {
    Object(fields) => fields
    _ => raise TopologyError("series sweep request must be an object")
  }
  let kind : String = @json.from_json(required_field(object, "kind")) catch {
    _ => raise TopologyError("kind must be a string")
  }
  if kind != "series_sweep" {
    raise TopologyError("kind must be series_sweep")
  }
  for field in object.keys() {
    if ![
        "kind", "series", "threshold", "dimensions", "lags", "stride", "max_cells",
      ].contains(field) {
      raise TopologyError("unknown series sweep field: " + field)
    }
  }
  let series : Array[Double] = @json.from_json(required_field(object, "series")) catch {
    _ => raise TopologyError("series must be an array of numbers")
  }
  let threshold = number_field(object, "threshold")
  if !finite(threshold) || threshold <= 0.0 || threshold > 1.0e100 {
    raise TopologyError("sweep threshold must be finite and positive")
  }
  let dimensions = sweep_integer_list(object, "dimensions", 1, 32)
  let lags = sweep_integer_list(object, "lags", 1, 10000)
  if dimensions.length() * lags.length() > 12 {
    raise TopologyError(
      "series sweep supports at most 12 parameter combinations",
    )
  }
  let stride = integer_field(object, "stride", 1)
  let budget = integer_field(object, "max_cells", 4000)
  let grid = landscape_grid(0.0, threshold, 101)
  let cases : Array[Json] = []
  let candidates : Array[SweepCandidate] = []
  let mut summary_csv = "dimension,lag,stride,point_count,cell_count,h1_finite_count,h1_alive_at_cutoff,longest_h1_observed_lifetime,h1_at_cutoff\n"
  let mut vector_csv = "dimension,lag,stride"
  for layer = 1; layer <= 3; layer = layer + 1 {
    for sample = 0; sample < 101; sample = sample + 1 {
      vector_csv += ",lambda_\{layer}_sample_\{sample}"
    }
  }
  vector_csv += "\n"
  let mut total_cells = 0
  for dimension in dimensions {
    for lag in lags {
      let points = delay_embed(series, dimension~, lag~, stride~)
      let analysis = analyze(rips(points, threshold~, max_cells=budget))
      let cell_count = analysis.filtration.cells.length()
      total_cells += cell_count
      if total_cells > 16000 {
        raise TopologyError("series sweep exceeds 16000 total filtration cells")
      }
      let mut finite_count = 0
      let mut alive_count = 0
      let mut longest_observed = 0.0
      let mut longest_finite = 0.0
      for interval in analysis.intervals {
        if interval.dimension != 1 {
          continue
        }
        if interval.death is None {
          alive_count += 1
        } else {
          finite_count += 1
          longest_finite = maximum(
            longest_finite,
            observed_lifetime(analysis, interval),
          )
        }
        longest_observed = maximum(
          longest_observed,
          observed_lifetime(analysis, interval),
        )
      }
      let landscape = persistence_landscape(
        analysis,
        dimension=1,
        start=0.0,
        end=threshold,
      )
      let vector : Array[Double] = []
      for layer in landscape {
        for sample in layer {
          vector.push(sample)
        }
      }
      let h1_at_cutoff = betti(analysis, 1, threshold)
      cases.push({
        "dimension": dimension,
        "lag": lag,
        "stride": stride,
        "point_count": points.length(),
        "cell_count": cell_count,
        "h1_finite_count": finite_count,
        "h1_alive_at_cutoff": alive_count,
        "longest_h1_observed_lifetime": longest_observed,
        "longest_finite_h1_lifetime": longest_finite,
        "h1_at_cutoff": h1_at_cutoff,
        "h1_landscape_vector": vector,
      })
      if finite_count > 0 {
        candidates.push({
          dimension,
          lag,
          stride,
          point_count: points.length(),
          finite_h1_count: finite_count,
          longest_finite_h1_lifetime: longest_finite,
        })
      }
      summary_csv += "\{dimension},\{lag},\{stride},\{points.length()},\{cell_count},\{finite_count},\{alive_count},\{longest_observed},\{h1_at_cutoff}\n"
      vector_csv += "\{dimension},\{lag},\{stride}"
      for sample in vector {
        vector_csv += ",\{sample}"
      }
      vector_csv += "\n"
    }
  }
  candidates.sort_by(fn(a, b) {
    if a.longest_finite_h1_lifetime > b.longest_finite_h1_lifetime {
      -1
    } else if a.longest_finite_h1_lifetime < b.longest_finite_h1_lifetime {
      1
    } else if a.finite_h1_count != b.finite_h1_count {
      b.finite_h1_count - a.finite_h1_count
    } else if a.dimension != b.dimension {
      a.dimension - b.dimension
    } else {
      a.lag - b.lag
    }
  })
  let recommendations : Array[Json] = []
  for i = 0; i < candidates.length() && i < 3; i = i + 1 {
    let candidate = candidates[i]
    recommendations.push({
      "rank": i + 1,
      "dimension": candidate.dimension,
      "lag": candidate.lag,
      "stride": candidate.stride,
      "point_count": candidate.point_count,
      "finite_h1_count": candidate.finite_h1_count,
      "longest_finite_h1_lifetime": candidate.longest_finite_h1_lifetime,
      "reason": "ranked by longest finite H1 lifetime, then finite H1 count; only among the supplied combinations",
    })
  }
  (
    {
      "schema_version": 1,
      "kind": "series_sweep",
      "threshold": threshold,
      "dimensions": dimensions,
      "lags": lags,
      "stride": stride,
      "case_count": cases.length(),
      "total_cells": total_cells,
      "landscape": {
        "homology_dimension": 1,
        "samples": 101,
        "layers": 3,
        "grid": grid,
        "vector_order": "layer-major, then increasing scale",
        "normalization": "none; raw tent heights",
        "censoring": "alive-at-cutoff intervals are excluded from landscape vectors",
      },
      "cases": cases,
      "recommended_parameters": recommendations,
      "recommendation_note": "exploratory ordering of supplied combinations only; no automatic period estimate, lag optimum or embedding-dimension guarantee; different combinations may contain different point counts",
      "interpretation": "parameter sensitivity only; dimensions and lags change the embedded point cloud and may change its point count",
    },
    summary_csv,
    vector_csv,
  )
}