///|
fn find_arg(args : Array[String], name : String) -> String? {
  for i = 0; i < args.length(); i = i + 1 {
    if args[i] == name && i + 1 < args.length() {
      return Some(args[i + 1])
    }
  }
  None
}

///|
fn parse_double_arg(
  args : Array[String],
  name : String,
  default : Double,
) -> Double {
  match find_arg(args, name) {
    Some(value) => @string.parse_double(value) catch { _ => default }
    None => default
  }
}

///|
fn print_help() -> Unit {
  println("MoonTide - local-first tide and coastal activity prediction")
  println("")
  println("Usage:")
  println("  moontide list")
  println("  moontide validate --station demo")
  println(
    "  moontide predict --station demo --date 2026-10-12 --tz 8 --hours 24",
  )
  println("  moontide stats --station demo --date 2026-10-12 --hours 48")
  println("  moontide events --station demo --date 2026-10-12 --format csv")
  println("  moontide curve --station demo --date 2026-10-12")
  println("  moontide plan --station demo --date 2026-10-12")
  println("  moontide export --station demo --date 2026-10-12 --format ics")
  println(
    "  moontide validate-reference --metadata  --harcon  --datums  --predictions ",
  )
  println("")
  println("Common options:")
  println("  --station          Built-in station: demo, qingdao, xiamen")
  println("  --date     Prediction start date")
  println("  --tz           UTC offset in hours, default 8")
  println("  --hours             Prediction duration, default 24")
  println("  --format          markdown, json, csv, or ics")
  println("  --metadata       NOAA station metadata JSON")
  println("  --harcon         NOAA harmonic constituents JSON")
  println("  --datums         NOAA datum JSON")
  println("  --predictions    NOAA prediction JSON")
  println("  --tolerance         Mean-error threshold, default 0.3 m")
}

///|
fn load_station(args : Array[String]) -> @lib.Station? {
  let id = find_arg(args, "--station").unwrap_or("demo")
  match @lib.find_station(id) {
    Some(station) => Some(station)
    None => {
      println("Unknown built-in station: " + id)
      None
    }
  }
}

///|
fn load_prediction(args : Array[String]) -> @lib.Prediction? {
  let station = match load_station(args) {
    Some(value) => value
    None => return None
  }
  let date = find_arg(args, "--date").unwrap_or("2026-10-12")
  let tz = parse_double_arg(args, "--tz", 8.0)
  let hours = parse_double_arg(args, "--hours", 24.0)
  match @lib.predict_on(station, date, tz, hours) {
    Some(prediction) => Some(prediction)
    None => {
      println("Invalid date (expected YYYY-MM-DD or YYYY-MM-DD HH:MM): " + date)
      None
    }
  }
}

///|
fn list_stations() -> Unit {
  println(@lib.stations_to_markdown(@lib.builtin_stations()))
}

///|
fn validate_station(args : Array[String]) -> Unit {
  let station = match load_station(args) {
    Some(value) => value
    None => return
  }
  let tz = parse_double_arg(args, "--tz", 8.0)
  let hours = parse_double_arg(args, "--hours", 24.0)
  let issues = @lib.validate_station(station)
  issues.append(@lib.validate_timezone(tz))
  issues.append(@lib.validate_hours(hours))
  println(@lib.validation_to_markdown(issues))
}

///|
fn print_stats(args : Array[String]) -> Unit {
  let prediction = match load_prediction(args) {
    Some(value) => value
    None => return
  }
  let samples = @lib.sample_series(
    prediction.station,
    prediction.start_jd,
    prediction.duration_hours,
    1.0 / 12.0,
  )
  let stats = @lib.compute_stats(samples)
  let coefficient = @lib.tide_coefficient(
    stats.range,
    @lib.mean_tidal_range(prediction.station),
  )
  println(@lib.stats_to_markdown(stats, coefficient))
  println("")
  println("Dominant state: " + @lib.state_label(@lib.dominant_state(samples)))
}

///|
fn print_events(args : Array[String]) -> Unit {
  let prediction = match load_prediction(args) {
    Some(value) => value
    None => return
  }
  let format = find_arg(args, "--format").unwrap_or("markdown")
  if format == "csv" {
    println(@lib.extremes_to_csv(prediction.extremes, prediction.tz))
  } else if format == "json" {
    println(@lib.render_json(prediction))
  } else {
    for extreme in prediction.extremes {
      let kind = if extreme.kind == @lib.ExtremeKind::High {
        "High"
      } else {
        "Low"
      }
      println(
        @lib.format_local_datetime(extreme.time_jd, prediction.tz) +
        "  " +
        kind +
        "  " +
        @lib.format_meters(extreme.height) +
        " m",
      )
    }
  }
}

///|
fn print_curve(args : Array[String]) -> Unit {
  let prediction = match load_prediction(args) {
    Some(value) => value
    None => return
  }
  println(
    @lib.render_curve(
      prediction.station,
      prediction.start_jd,
      prediction.duration_hours,
      1.0,
      prediction.tz,
    ),
  )
}

///|
fn print_plan(args : Array[String]) -> Unit {
  let prediction = match load_prediction(args) {
    Some(value) => value
    None => return
  }
  println(@lib.plans_to_markdown(@lib.plans_for(prediction), prediction.tz))
}

///|
fn export_prediction(args : Array[String]) -> Unit {
  let prediction = match load_prediction(args) {
    Some(value) => value
    None => return
  }
  let format = find_arg(args, "--format").unwrap_or("csv")
  if format == "ics" {
    println(
      @lib.extremes_to_ics(
        prediction.station,
        prediction.extremes,
        prediction.tz,
      ),
    )
  } else if format == "json" {
    println(@lib.render_json(prediction))
  } else {
    let samples = @lib.sample_series(
      prediction.station,
      prediction.start_jd,
      prediction.duration_hours,
      1.0 / 12.0,
    )
    println(@lib.samples_to_csv(samples, prediction.tz))
  }
}

///|
fn read_json_file(path : String) -> Json? {
  let text = @fs.read_file_to_string(path) catch {
    @fs.IOError(message) => {
      println("Failed to read " + path + ": " + message)
      return None
    }
  }
  let json = @json.parse(text) catch {
    _ => {
      println("Invalid JSON: " + path)
      return None
    }
  }
  Some(json)
}

///|
fn validate_reference(args : Array[String]) -> Unit {
  let metadata_path = match find_arg(args, "--metadata") {
    Some(value) => value
    None => {
      println("--metadata is required")
      return
    }
  }
  let harcon_path = match find_arg(args, "--harcon") {
    Some(value) => value
    None => {
      println("--harcon is required")
      return
    }
  }
  let predictions_path = match find_arg(args, "--predictions") {
    Some(value) => value
    None => {
      println("--predictions is required")
      return
    }
  }
  let metadata = match read_json_file(metadata_path) {
    Some(value) => value
    None => return
  }
  let harcon = match read_json_file(harcon_path) {
    Some(value) => value
    None => return
  }
  let datums = match find_arg(args, "--datums") {
    Some(path) => read_json_file(path)
    None => None
  }
  let station = match @lib.station_from_noaa(metadata, harcon, datums?) {
    Some(value) => value
    None => {
      println("Failed to build a station from NOAA data")
      return
    }
  }
  let unsupported = @lib.noaa_unsupported_constituents(harcon)
  let reference_json = match read_json_file(predictions_path) {
    Some(value) => value
    None => return
  }
  let references = match @lib.predictions_from_noaa(reference_json) {
    Some(value) => value
    None => {
      println("Failed to parse NOAA predictions")
      return
    }
  }
  let tolerance = parse_double_arg(args, "--tolerance", 0.3)
  let stats = match
    @lib.compare_with_reference(station, references, tolerance) {
    Some(value) => value
    None => {
      println("No usable reference samples")
      return
    }
  }
  println("# MoonTide NOAA Validation")
  println("")
  println("- Station: " + station.name + " (" + station.id + ")")
  println("- Source: " + station.source)
  println(
    "- Datum offset (MSL - MLLW): " + @lib.format_meters(station.datum) + " m",
  )
  println(
    "- Harmonic constituents: " +
    (station.components.length() + unsupported.length()).to_string() +
    " total, " +
    station.components.length().to_string() +
    " used, " +
    unsupported.length().to_string() +
    " ignored",
  )
  if unsupported.length() > 0 {
    println("- Ignored by the current catalog: " + unsupported.join(", "))
  }
  println(@lib.validation_stats_to_markdown(stats))
  if !stats.passed {
    abort("NOAA validation exceeded the configured mean-error tolerance")
  }
}

///|
fn main {
  let args = @env.args()
  if args.contains("--help") || args.contains("-h") {
    print_help()
    return
  }
  if args.contains("list") {
    list_stations()
    return
  }
  if args.contains("validate-reference") {
    validate_reference(args)
    return
  }
  if args.contains("validate") {
    validate_station(args)
    return
  }
  if args.contains("stats") {
    print_stats(args)
    return
  }
  if args.contains("events") {
    print_events(args)
    return
  }
  if args.contains("curve") {
    print_curve(args)
    return
  }
  if args.contains("plan") {
    print_plan(args)
    return
  }
  if args.contains("export") {
    export_prediction(args)
    return
  }
  if !args.contains("predict") {
    print_help()
    return
  }
  let prediction = match load_prediction(args) {
    Some(value) => value
    None => return
  }
  let format = find_arg(args, "--format").unwrap_or("markdown")
  if format == "json" {
    println(@lib.render_json(prediction))
  } else {
    println(@lib.render_report(prediction))
  }
}