///|
/// Stable categories for command-line argument and PLAN parsing errors.
///
/// Codes distinguish missing, duplicate, and unknown arguments from malformed
/// or duplicate `LAP:COMPOUND` plan entries.
pub(all) enum CliErrorCode {
  MissingInputPath
  MissingRequiredOption
  MissingOptionValue
  DuplicateOption
  UnknownArgument
  UnexpectedPositional
  InvalidPlanFormat
  InvalidPlanLap
  InvalidPlanCompound
  DuplicatePlanLap
} derive(Eq)

///|
pub extend CliErrorCode with Eq::{not_equal, equal}

///|

///|
/// A deterministic, user-facing CLI parsing error.
///
/// `field` identifies the option or logical input field; `code` is stable for
/// programmatic handling and `message` is suitable for stderr output.
pub struct CliError {
  code : CliErrorCode
  field : String
  message : String
} derive(Eq)

///|
pub extend CliError with Eq::{not_equal, equal}

///|

///|
/// Parsed CLI input. File paths remain at the command boundary; simulation stays pure.
///
/// `target_driver` receives the replacement plan in `stops`; `opponent_driver`
/// is used for signed-gap comparison. `output_path` selects file output when present.
pub struct CliOptions {
  input_path : String
  target_driver : String
  opponent_driver : String
  stops : Array[PlannedPitStop]
  output_path : String?
} derive(Eq)

///|
pub extend CliOptions with Eq::{not_equal, equal}

///|

///|
/// Stable categories for the pure CSV-to-report pipeline.
///
/// Codes identify whether CSV validation, strategy simulation, or explanation
/// failed while producing a Markdown report.
pub(all) enum CliPipelineErrorCode {
  CsvValidationFailed
  StrategySimulationFailed
  ExplanationFailed
} derive(Eq)

///|
pub extend CliPipelineErrorCode with Eq::{not_equal, equal}

///|

///|
/// A user-facing error from the pure CSV-to-Markdown pipeline.
///
/// This keeps file I/O out of the core pipeline while exposing a stable failure
/// phase through `code` and a concise diagnostic through `message`.
pub struct CliPipelineError {
  code : CliPipelineErrorCode
  message : String
} derive(Eq)

///|
pub extend CliPipelineError with Eq::{not_equal, equal}

///|

///|
fn cli_error(code : CliErrorCode, field : String, message : String) -> CliError {
  { code, field, message, }
}

///|
fn parse_positive_plan_lap(text : String) -> Result[Int, CliError] {
  if text == "" {
    return Err(
      cli_error(
        InvalidPlanLap,
        "stops",
        "pit lap must be a positive decimal integer",
      ),
    )
  }
  let mut value = 0
  for char in text {
    if !char.is_ascii_digit() {
      return Err(
        cli_error(
          InvalidPlanLap,
          "stops",
          "pit lap must be a positive decimal integer",
        ),
      )
    }
    let digit = char.to_int() - '0'.to_int()
    if value > 214748364 || (value == 214748364 && digit > 7) {
      return Err(
        cli_error(
          InvalidPlanLap,
          "stops",
          "pit lap is outside the supported range",
        ),
      )
    }
    value = value * 10 + digit
  }
  if value < 1 {
    Err(
      cli_error(
        InvalidPlanLap,
        "stops",
        "pit lap must be a positive decimal integer",
      ),
    )
  } else {
    Ok(value)
  }
}

///|
fn parse_plan_compound(text : String) -> Result[Compound, CliError] {
  match text {
    "SOFT" => Ok(Soft)
    "MEDIUM" => Ok(Medium)
    "HARD" => Ok(Hard)
    "INTERMEDIATE" => Ok(Intermediate)
    "WET" => Ok(Wet)
    _ =>
      Err(
        cli_error(
          InvalidPlanCompound,
          "stops",
          "PLAN compound must be SOFT, MEDIUM, HARD, INTERMEDIATE, or WET",
        ),
      )
  }
}

///|
/// Parse `none` or a comma-separated sequence of `LAP:COMPOUND` pit stops.
///
/// Each lap must be a positive decimal integer and each compound is one of the
/// CSV v1 uppercase values. Duplicate laps and malformed items return a
/// structured `CliError`; lap range and ordering remain M4 validation concerns.
pub fn parse_stop_plan(
  plan : String,
) -> Result[Array[PlannedPitStop], CliError] {
  if plan == "none" {
    return Ok([])
  }
  if plan == "" {
    return Err(
      cli_error(
        InvalidPlanFormat,
        "stops",
        "PLAN must be `none` or comma-separated LAP:COMPOUND items",
      ),
    )
  }
  let stops : Array[PlannedPitStop] = []
  let items = plan.split(",").to_array()
  for raw_item in items {
    let item = raw_item.to_owned()
    if item == "" || item.trim().to_owned() != item {
      return Err(
        cli_error(
          InvalidPlanFormat,
          "stops",
          "PLAN must not contain empty or whitespace-padded items",
        ),
      )
    }
    let parts = item.split(":").to_array()
    if parts.length() != 2 || parts[0] == "" || parts[1] == "" {
      return Err(
        cli_error(
          InvalidPlanFormat,
          "stops",
          "each PLAN item must use the exact LAP:COMPOUND format",
        ),
      )
    }
    let pit_lap = match parse_positive_plan_lap(parts[0].to_owned()) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let next_compound = match parse_plan_compound(parts[1].to_owned()) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    for previous in stops {
      if previous.pit_lap == pit_lap {
        return Err(
          cli_error(
            DuplicatePlanLap,
            "stops",
            "PLAN must not contain duplicate pit laps",
          ),
        )
      }
    }
    stops.push({ pit_lap, next_compound, })
  }
  Ok(stops)
}

///|
fn option_requires_value(argument : String) -> Bool {
  match argument {
    "--driver" | "--opponent" | "--stops" | "-o" | "--output" => true
    _ => false
  }
}

///|
fn is_option(argument : String) -> Bool {
  argument.has_prefix("-")
}

///|
/// Parse program arguments after the executable prefix in any option order.
///
/// Requires one CSV path plus `--driver`, `--opponent`, and `--stops`; `-o` or
/// `--output` is optional. Returns `CliOptions` without file I/O or a
/// structured `CliError` for missing, duplicate, unknown, or malformed input.
pub fn parse_cli_arguments(
  arguments : Array[String],
) -> Result[CliOptions, CliError] {
  let mut input_path : String? = None
  let mut target_driver : String? = None
  let mut opponent_driver : String? = None
  let mut plan : String? = None
  let mut output_path : String? = None
  let mut index = 0
  while index < arguments.length() {
    let argument = arguments[index]
    if option_requires_value(argument) {
      if index + 1 >= arguments.length() || is_option(arguments[index + 1]) {
        return Err(
          cli_error(
            MissingOptionValue,
            argument,
            "option " + argument + " requires a value",
          ),
        )
      }
      let value = arguments[index + 1]
      match argument {
        "--driver" =>
          match target_driver {
            Some(_) =>
              return Err(
                cli_error(
                  DuplicateOption,
                  "driver",
                  "--driver may be provided only once",
                ),
              )
            None => target_driver = Some(value)
          }
        "--opponent" =>
          match opponent_driver {
            Some(_) =>
              return Err(
                cli_error(
                  DuplicateOption,
                  "opponent",
                  "--opponent may be provided only once",
                ),
              )
            None => opponent_driver = Some(value)
          }
        "--stops" =>
          match plan {
            Some(_) =>
              return Err(
                cli_error(
                  DuplicateOption,
                  "stops",
                  "--stops may be provided only once",
                ),
              )
            None => plan = Some(value)
          }
        "-o" | "--output" =>
          match output_path {
            Some(_) =>
              return Err(
                cli_error(
                  DuplicateOption,
                  "output",
                  "-o/--output may be provided only once",
                ),
              )
            None => output_path = Some(value)
          }
        _ => ()
      }
      index = index + 2
    } else if is_option(argument) {
      return Err(
        cli_error(UnknownArgument, "argument", "unknown option: " + argument),
      )
    } else {
      match input_path {
        None => input_path = Some(argument)
        Some(_) =>
          return Err(
            cli_error(
              UnexpectedPositional,
              "input",
              "only one input CSV path is accepted",
            ),
          )
      }
      index = index + 1
    }
  }
  let input_path = match input_path {
    Some(value) => value
    None =>
      return Err(
        cli_error(MissingInputPath, "input", "missing required input CSV path"),
      )
  }
  let target_driver = match target_driver {
    Some(value) => value
    None =>
      return Err(
        cli_error(
          MissingRequiredOption,
          "driver",
          "missing required --driver option",
        ),
      )
  }
  let opponent_driver = match opponent_driver {
    Some(value) => value
    None =>
      return Err(
        cli_error(
          MissingRequiredOption,
          "opponent",
          "missing required --opponent option",
        ),
      )
  }
  let plan = match plan {
    Some(value) => value
    None =>
      return Err(
        cli_error(
          MissingRequiredOption,
          "stops",
          "missing required --stops option",
        ),
      )
  }
  let stops = match parse_stop_plan(plan) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok({ input_path, target_driver, opponent_driver, stops, output_path, })
}

///|
/// Convert validated CSV text into the stable M5 Markdown strategy report.
///
/// The pure pipeline parses CSV, applies the default illustrative pace model,
/// simulates `stops` for `target_driver`, explains the result against
/// `opponent_driver`, and renders Markdown. Returns `CliPipelineError` with a
/// stable phase code for CSV, simulation, or explanation failure.
pub fn generate_strategy_report(
  csv : String,
  target_driver : String,
  opponent_driver : String,
  stops : Array[PlannedPitStop],
) -> Result[String, CliPipelineError] {
  let data = match parse_race_csv(csv) {
    Ok(value) => value
    Err(error) =>
      return Err({
        code: CsvValidationFailed,
        message: "CSV validation failed at line " +
        error.line.to_string() +
        " (" +
        error.field +
        "): " +
        error.message,
      })
  }
  let request = { target_driver, opponent_driver, stops, }
  let simulation = match
    simulate_strategy(data, default_pace_model_config(), request) {
    Ok(value) => value
    Err(error) =>
      return Err({
        code: StrategySimulationFailed,
        message: "strategy simulation failed: " + error.message,
      })
  }
  match explain_strategy(default_pace_model_config(), simulation) {
    Ok(explanation) => Ok(render_strategy_markdown(explanation))
    Err(error) =>
      Err({
        code: ExplanationFailed,
        message: "strategy explanation failed: " + error.message,
      })
  }
}

///|
/// Return the stable, human-readable CLI usage text.
///
/// This has no side effects and describes the executable syntax, PLAN grammar,
/// supported compounds, output option, and help option.
pub fn cli_help() -> String {
  (
    #|RaceDelta — race strategy analyzer and counterfactual simulator
    #|
    #|Usage:
    #|  racedelta  --driver  --opponent  --stops  [-o ]
    #|
    #|Required:
    #|               CSV v1 race data
    #|  --driver        Target driver for the alternative strategy
    #|  --opponent      Driver used for comparison
    #|  --stops           none or LAP:COMPOUND[,LAP:COMPOUND...]
    #|
    #|Optional:
    #|  -o, --output      Write Markdown to PATH instead of stdout
    #|  -h, --help              Show this help text
    #|
    #|Compounds: SOFT, MEDIUM, HARD, INTERMEDIATE, WET
    #|
  )
}