///|
pub suberror CliError {
  MissingArgument(String)
  InvalidNumber(String, String)
  UnknownCommand(String)
  InvalidLanguage(String)
} derive(Eq, Debug)

///|
pub struct CliResult {
  exit_code : Int
  stdout : String
  stderr : String
} derive(Eq, Debug)

///|
fn cli_success(output : String) -> CliResult {
  { exit_code: 0, stdout: output, stderr: "" }
}

///|
fn cli_failure(code : Int, message : String) -> CliResult {
  { exit_code: code, stdout: "", stderr: "error: " + message }
}

///|
pub fn cli_help() -> String {
  [
    "RRuleLab - explainable RFC 5545 recurrence laboratory", "", "USAGE", "  rrule_lab  [arguments]",
    "", "COMMANDS", "  validate ", "  normalize ", "  explain  [en|zh]",
    "  expand     [LIMIT]", "  audit     [LIMIT]",
    "  diff     ", "  duration  [DTSTART]",
    "  demo", "  version", "", "DATE-TIME", "  Basic 20260801T090000 or extended 2026-08-01T09:00:00.",
    "", "EXAMPLES", "  rrule_lab explain 'FREQ=WEEKLY;BYDAY=MO,WE' zh", "  rrule_lab expand 20260801T090000 'FREQ=DAILY;COUNT=3' 20260801T000000 20260810T000000",
  ].join("\n")
}

///|
fn require_cli_arg(
  args : Array[String],
  index : Int,
  label : String,
) -> String raise CliError {
  if index >= args.length() {
    raise MissingArgument(label)
  }
  args[index]
}

///|
fn parse_cli_int(text : String, label : String) -> Int raise CliError {
  @string.parse_int(text, base=10) catch {
    _ => raise InvalidNumber(label, text)
  }
}

///|
fn diagnostics_text(diagnostics : Array[Diagnostic]) -> String {
  if diagnostics.length() == 0 {
    return "valid"
  }
  let lines : Array[String] = []
  for item in diagnostics {
    let severity = match item.severity {
      Error => "error"
      Warning => "warning"
      Information => "info"
    }
    lines.push(
      "[" + severity + "] " + item.code + " " + item.field + ": " + item.message,
    )
  }
  lines.join("\n")
}

///|
fn datetime_lines(values : Array[DateTime]) -> String {
  if values.length() == 0 {
    return "(no occurrences)"
  }
  values.map(value => value.to_iso_string()).join("\n")
}

///|
fn expansion_options_from_cli(
  start_text : String,
  end_text : String,
  limit_text : String?,
) -> ExpansionOptions raise {
  let limit = match limit_text {
    Some(value) => parse_cli_int(value, "LIMIT")
    None => 100
  }
  ExpansionOptions::new(
    parse_datetime(start_text),
    parse_datetime(end_text),
    limit~,
  )
}

///|
fn run_validate(args : Array[String]) -> CliResult raise {
  let rule = parse_rrule(require_cli_arg(args, 1, "RRULE"))
  let diagnostics = rule.validate()
  let has_error = diagnostics.any(item => item.severity == Error)
  {
    exit_code: if has_error {
      2
    } else {
      0
    },
    stdout: diagnostics_text(diagnostics),
    stderr: "",
  }
}

///|
fn run_normalize(args : Array[String]) -> CliResult raise {
  let rule = parse_rrule(require_cli_arg(args, 1, "RRULE"))
  cli_success(rule.to_string())
}

///|
fn run_explain(args : Array[String]) -> CliResult raise {
  let rule = parse_rrule(require_cli_arg(args, 1, "RRULE"))
  let language = if args.length() > 2 { args[2].to_lower() } else { "en" }
  match language {
    "en" => cli_success(rule.explain_en())
    "zh" | "zh-cn" => cli_success(rule.explain_zh())
    _ => raise InvalidLanguage(language)
  }
}

///|
fn run_expand(args : Array[String]) -> CliResult raise {
  let start = parse_datetime(require_cli_arg(args, 1, "DTSTART"))
  let rule = parse_rrule(require_cli_arg(args, 2, "RRULE"))
  let window_start = require_cli_arg(args, 3, "WINDOW_START")
  let window_end = require_cli_arg(args, 4, "WINDOW_END")
  let limit = if args.length() > 5 { Some(args[5]) } else { None }
  cli_success(
    datetime_lines(
      expand_rrule(
        start,
        rule,
        expansion_options_from_cli(window_start, window_end, limit),
      ),
    ),
  )
}

///|
fn run_audit(args : Array[String]) -> CliResult raise {
  let start = parse_datetime(require_cli_arg(args, 1, "DTSTART"))
  let rule = parse_rrule(require_cli_arg(args, 2, "RRULE"))
  let window_start = require_cli_arg(args, 3, "WINDOW_START")
  let window_end = require_cli_arg(args, 4, "WINDOW_END")
  let limit = if args.length() > 5 { Some(args[5]) } else { None }
  let audit = audit_rule(
    start,
    rule,
    expansion_options_from_cli(window_start, window_end, limit),
  )
  cli_success(audit.to_text())
}

///|
fn impact_text(impact : ChangeImpact) -> String {
  let lines : Array[String] = [
    "baseline-count: " + impact.baseline_count.to_string(),
    "candidate-count: " + impact.candidate_count.to_string(),
    "similarity-permille: " + impact.similarity_permille.to_string(),
    "added:",
  ]
  if impact.added.length() == 0 {
    lines.push("  (none)")
  } else {
    for value in impact.added {
      lines.push("  + " + value.to_iso_string())
    }
  }
  lines.push("removed:")
  if impact.removed.length() == 0 {
    lines.push("  (none)")
  } else {
    for value in impact.removed {
      lines.push("  - " + value.to_iso_string())
    }
  }
  lines.join("\n")
}

///|
fn run_diff(args : Array[String]) -> CliResult raise {
  let start = parse_datetime(require_cli_arg(args, 1, "DTSTART"))
  let baseline = parse_rrule(require_cli_arg(args, 2, "OLD_RRULE"))
  let candidate = parse_rrule(require_cli_arg(args, 3, "NEW_RRULE"))
  let options = expansion_options_from_cli(
    require_cli_arg(args, 4, "WINDOW_START"),
    require_cli_arg(args, 5, "WINDOW_END"),
    None,
  )
  cli_success(
    impact_text(analyze_rule_change(start, baseline, candidate, options)),
  )
}

///|
fn run_duration(args : Array[String]) -> CliResult raise {
  let value = parse_duration(require_cli_arg(args, 1, "ISO_DURATION"))
  let lines : Array[String] = [
    "normalized: " + value.to_iso_string(),
    "english: " + value.explain_en(),
    "chinese: " + value.explain_zh(),
  ]
  if args.length() > 2 {
    let start = parse_datetime(args[2])
    lines.push("result: " + start.add_duration(value).to_iso_string())
  }
  cli_success(lines.join("\n"))
}

///|
fn run_demo() -> CliResult raise {
  let start = parse_datetime("20260803T093000")
  let rule = parse_rrule("FREQ=WEEKLY;COUNT=8;BYDAY=MO,WE,FR")
  let options = ExpansionOptions::new(
    parse_datetime("20260801T000000"),
    parse_datetime("20260901T000000"),
    limit=20,
  )
  let values = expand_rrule(start, rule, options)
  let audit = audit_occurrences(values)
  cli_success(
    [
      "rule: " + rule.to_string(),
      "explanation: " + rule.explain_en(),
      "",
      datetime_lines(values),
      "",
      audit.to_text(),
    ].join("\n"),
  )
}

///|
/// Execute one CLI command without performing process I/O. This design makes
/// the command interface fully testable and portable across Wasm and JS.
pub fn execute_cli(args : Array[String]) -> CliResult {
  if args.length() == 0 {
    return cli_success(cli_help())
  }
  try {
    match args[0].to_lower() {
      "help" | "--help" | "-h" => cli_success(cli_help())
      "version" | "--version" | "-v" => cli_success(version())
      "validate" => run_validate(args)
      "normalize" => run_normalize(args)
      "explain" => run_explain(args)
      "expand" => run_expand(args)
      "audit" => run_audit(args)
      "diff" => run_diff(args)
      "duration" => run_duration(args)
      "demo" => run_demo()
      command => raise UnknownCommand(command)
    }
  } catch {
    MissingArgument(label) => cli_failure(64, "missing argument " + label)
    InvalidNumber(label, value) =>
      cli_failure(64, label + " is not an integer: " + value)
    UnknownCommand(command) => cli_failure(64, "unknown command: " + command)
    InvalidLanguage(language) =>
      cli_failure(64, "unsupported language: " + language)
    error => cli_failure(2, error.to_string())
  }
}