///|
/// Parser error representation.
pub suberror HrvParseError {
  InvalidNumber(input~ : String)
  InvalidRow(msg~ : String)
  EmptyInput
} derive(Debug, ToJson, FromJson)

///|
/// Trim whitespace from String to StringView
fn trim(s : String) -> StringView {
  let len = s.length()
  let mut start = 0
  while start < len &&
        (s[start] == 32 || s[start] == 9 || s[start] == 10 || s[start] == 13) {
    start += 1
  }
  let mut end = len
  while end > start &&
        (
          s[end - 1] == 32 ||
          s[end - 1] == 9 ||
          s[end - 1] == 10 ||
          s[end - 1] == 13
        ) {
    end -= 1
  }
  s[start:end]
}

///|
/// CSV Parser using a state-machine. Handles optional double quotes and escapes.
pub fn parse_csv(content : String) -> Array[Array[String]] {
  let result = []
  let mut row = []
  let mut cell = StringBuilder::new()
  let mut in_quotes = false
  let len = content.length()
  let mut i = 0

  while i < len {
    let c = content[i]
    if in_quotes {
      if c == 34 {
        if i + 1 < len && content[i + 1] == 34 {
          cell.write_char('"')
          i += 1
        } else {
          in_quotes = false
        }
      } else {
        cell.write_char(c.to_int().unsafe_to_char())
      }
    } else if c == 34 {
      in_quotes = true
    } else if c == 44 {
      row.push(cell.to_string())
      cell = StringBuilder::new()
    } else if c == 10 {
      row.push(cell.to_string())
      cell = StringBuilder::new()
      result.push(row)
      row = []
    } else if c == 13 {
      if i + 1 < len && content[i + 1] == 10 {
        row.push(cell.to_string())
        cell = StringBuilder::new()
        result.push(row)
        row = []
        i += 1
      } else {
        cell.write_char(c.to_int().unsafe_to_char())
      }
    } else {
      cell.write_char(c.to_int().unsafe_to_char())
    }
    i += 1
  }

  if cell.to_string().length() > 0 || row.length() > 0 {
    row.push(cell.to_string())
    result.push(row)
  }

  result
}

///|
/// CSV Serializer. Escapes double quotes, commas, and newlines.
pub fn to_csv(grid : Array[Array[String]]) -> String {
  let sb = StringBuilder::new()
  for r in 0.. 0 {
        sb.write_string(",")
      }
      let cell = row[c]
      let needs_quotes = cell.contains(",") ||
        cell.contains("\"") ||
        cell.contains("\n") ||
        cell.contains("\r")
      if needs_quotes {
        sb.write_string("\"")
        for i in 0.. Array[Double] raise HrvParseError {
  let grid = parse_csv(content)
  let result = []
  for r in 0.. 0 {
        try {
          let val : Double = @strconv.from_str(trimmed)
          result.push(val)
        } catch {
          _ => raise HrvParseError::InvalidNumber(input=cell)
        }
      }
    }
  }
  if result.length() == 0 {
    raise HrvParseError::EmptyInput
  }
  result
}

///|
/// Parse a CSV string containing historical morning measurements.
pub fn parse_morning_csv(
  content : String,
) -> Array[MorningMeasurement] raise HrvParseError {
  let grid = parse_csv(content)
  if grid.length() <= 1 {
    raise HrvParseError::EmptyInput
  }

  let mut start_idx = 0
  let first_row = grid[0]
  if first_row.length() > 0 {
    let cell0 = trim(first_row[0])
    if cell0.length() >= 4 && (cell0[0] == 100 || cell0[0] == 68) { // 'd' or 'D'
      start_idx = 1
    }
  }

  let result = []
  for r in start_idx.. raise HrvParseError::InvalidNumber(input=row[1])
    }

    let sdnn = @strconv.from_str(trim(row[2])) catch {
      _ => raise HrvParseError::InvalidNumber(input=row[2])
    }

    let rr_mean = @strconv.from_str(trim(row[3])) catch {
      _ => raise HrvParseError::InvalidNumber(input=row[3])
    }

    result.push({ date, rmssd, sdnn, rr_mean })
  }

  result
}

///|
/// Serialize HrvMetrics to CSV string.
pub fn serialize_metrics_csv(metrics : HrvMetrics) -> String {
  let grid = [
    ["metric", "value"],
    ["mean_rr", metrics.mean_rr.to_string()],
    ["mean_hr", metrics.mean_hr.to_string()],
    ["sdnn", metrics.sdnn.to_string()],
    ["rmssd", metrics.rmssd.to_string()],
    ["pnn50", metrics.pnn50.to_string()],
    ["pnn_custom", metrics.pnn_custom.to_string()],
    ["total_beats", metrics.quality.total_beats.to_string()],
    ["valid_beats", metrics.quality.valid_beats.to_string()],
    ["ectopic_beats", metrics.quality.ectopic_beats.to_string()],
    ["clean_ratio", metrics.quality.clean_ratio.to_string()],
    ["is_low_quality", metrics.quality.is_low_quality.to_string()],
  ]
  to_csv(grid)
}

///|
/// Serialize morning baseline trends to CSV string.
pub fn serialize_trends_csv(trends : Array[MorningTrend]) -> String {
  let grid = [
    [
      "date", "rmssd", "rolling_avg", "rolling_sd", "normal_lower", "normal_upper",
      "status", "recovery_score",
    ],
  ]
  for i in 0..