///|
#warnings("-deprecated")
fn render_content(
  trace : @frontierlab.AlgorithmTrace,
  format : String,
  step : Int32,
) -> String raise {
  match format {
    "html" => @frontierlab.render_trace_html(trace)
    "svg" => @frontierlab.render_trace_svg(trace, step~)
    "json" => trace.encode_json()
    other => fail("Unknown format: \{other}")
  }
}

///|
fn decode_trace(
  content : String,
  validate~ : Bool = true,
) -> @frontierlab.AlgorithmTrace raise {
  let trace = @frontierlab.AlgorithmTrace::decode_json(content)
  if validate {
    trace.validate()
  }
  trace
}

///|
fn input_trace(
  inputs : Map[String, String],
  path : String,
  validate~ : Bool = true,
) -> @frontierlab.AlgorithmTrace raise {
  match inputs.get(path) {
    Some(content) => decode_trace(content, validate~)
    None => fail("Missing CLI input: \{path}")
  }
}

///|
fn cli_input_paths(args : Array[String]) -> Array[String] {
  if args.length() < 2 {
    return []
  }
  match args[1] {
    "diagnose" | "diverge" =>
      if args.length() >= 4 {
        [args[2], args[3]]
      } else {
        []
      }
    "render" | "analyze" | "validate" | "diff" | "breakpoints" | "verify" =>
      if args.length() >= 3 {
        [args[2]]
      } else {
        []
      }
    _ => []
  }
}

///|
fn read_cli_inputs(args : Array[String]) -> Map[String, String] raise {
  let inputs : Map[String, String] = Map([])
  for path in cli_input_paths(args) {
    inputs[path] = @fs.read_file_to_string(path)
  }
  inputs
}

///|
fn contract_by_name(
  name : String,
  object_id : String,
) -> @frontierlab.TraceContract raise {
  if object_id == "" {
    fail("--object is required")
  }
  match name {
    "sequence-transition" =>
      @frontierlab.sequence_transition_contract(object_id~)
    "insertion-sort-int" => @frontierlab.insertion_sort_int_contract(object_id~)
    "sorted-int-sequence" =>
      @frontierlab.sorted_int_sequence_contract(object_id~)
    "grid-path" => @frontierlab.grid_path_contract(object_id~)
    _ => fail("Unknown contract: \{name}")
  }
}

///|
fn demo(name : String) -> @frontierlab.AlgorithmTrace raise {
  match name {
    "insertion-sort" => @frontierlab.insertion_sort_demo_trace([5, 2, 4, 1, 3])
    "faulty-insertion-sort" => @frontierlab.faulty_insertion_sort_demo_trace()
    "union-find" => @frontierlab.union_find_demo_trace()
    "pathfinding" => @frontierlab.pathfinding_demo_trace()
    "fibonacci" => @frontierlab.fibonacci_dp_trace(10)
    "coin-change" => @frontierlab.coin_change_dp_trace([1, 3, 4], 6)
    "knapsack" =>
      @frontierlab.zero_one_knapsack_dp_trace([2, 3, 4], [4, 5, 6], 5)
    "lcs" => @frontierlab.lcs_dp_trace("ABCBDAB", "BDCABA")
    "red-black-tree" =>
      @frontierlab.red_black_tree_trace([
        @frontierlab.Insert(30),
        @frontierlab.Insert(10),
        @frontierlab.Insert(20),
        @frontierlab.Insert(40),
        @frontierlab.Find(20),
        @frontierlab.Find(99),
      ])
    other => fail("Unknown demo: \{other}")
  }
}

///|
fn execute_cli(
  args : Array[String],
  inputs : Map[String, String],
) -> Diagnosis raise {
  if args.contains("--version") {
    return Diagnosis::new(stdout="FrontierLab 0.10.0")
  }
  if args.length() < 2 || args.contains("--help") {
    return Diagnosis::new(stdout=usage())
  }
  match args[1] {
    "diagnose" => {
      if args.length() < 4 {
        fail("diagnose requires expected and actual trace files")
      }
      let expected = input_trace(inputs, args[2])
      let actual = input_trace(inputs, args[3])
      let contract = contract_by_name(
        required_option(args, "--contract"),
        required_option(args, "--object"),
      )
      let format = debug_format(args)
      let diagnosis = @frontierlab.diagnose_trace(actual, contract~, expected~)
      let artifacts : Array[(String, String)] = []
      collect_artifact(
        artifacts,
        counterexample_artifact(
          option(args, "--counterexample", ""),
          diagnosis.focused_slice,
        ),
      )
      collect_artifact(
        artifacts,
        diagnosis_report_artifact(option(args, "--report", ""), diagnosis),
      )
      Diagnosis::new(
        exit_code=if diagnosis.passed() { 0 } else { 2 },
        stdout=debug_result(
          "diagnose",
          format,
          diagnosis_text(diagnosis),
          diagnosis_json(diagnosis),
        ),
        artifacts~,
      )
    }
    "demo" => {
      if args.length() < 3 {
        fail("demo requires a name")
      }
      let trace = demo(args[2])
      output_execution(
        option(args, "--output", "-"),
        render_content(
          trace,
          option(args, "--format", "html"),
          trace.steps.length() - 1,
        ),
        summary=trace.summary_report(),
      )
    }
    "render" => {
      if args.length() < 3 {
        fail("render requires a trace file")
      }
      let trace = input_trace(inputs, args[2], validate=false)
      output_execution(
        option(args, "--output", "-"),
        render_content(
          trace,
          option(args, "--format", "html"),
          @strconv.parse_int(option(args, "--step", "-1")),
        ),
      )
    }
    "analyze" => {
      if args.length() < 3 {
        fail("analyze requires a trace file")
      }
      Diagnosis::new(stdout=input_trace(inputs, args[2]).summary_report())
    }
    "validate" => {
      if args.length() < 3 {
        fail("validate requires a trace file")
      }
      ignore(input_trace(inputs, args[2]))
      Diagnosis::new(stdout="valid: \{args[2]}")
    }
    "playground" =>
      output_execution(
        option(args, "--output", "-"),
        @frontierlab.render_trace_playground(),
      )
    "diff" => {
      if args.length() < 3 {
        fail("diff requires a trace file")
      }
      let diff = input_trace(inputs, args[2]).diff(
        from_step=@strconv.parse_int(option(args, "--from", "-1")),
        to_step=@strconv.parse_int(option(args, "--to", "0")),
      )
      Diagnosis::new(
        stdout=debug_result(
          "diff",
          debug_format(args),
          diff.report(),
          diff.to_json_string(),
        ),
      )
    }
    "breakpoints" => {
      if args.length() < 3 {
        fail("breakpoints requires a trace file")
      }
      let trace = input_trace(inputs, args[2])
      let target = parse_target(option(args, "--target", ""))
      let breakpoint = match target {
        Some(value) =>
          @frontierlab.TraceBreakpoint::new(
            event_kind=option(args, "--event", ""),
            target=value,
            role=option(args, "--role", ""),
            changed_only=args.contains("--changed-only"),
          )
        None =>
          @frontierlab.TraceBreakpoint::new(
            event_kind=option(args, "--event", ""),
            role=option(args, "--role", ""),
            changed_only=args.contains("--changed-only"),
          )
      }
      let hits = trace.breakpoint_hits(breakpoint)
      let out = StringBuilder()
      out.write_string("Breakpoint hits: \{hits.length()}\n")
      for hit in hits {
        out.write_string(
          "- step \{hit.step}: \{hit.event_kind} (\{hit.reason})\n",
        )
      }
      Diagnosis::new(
        stdout=debug_result(
          "breakpoints",
          debug_format(args),
          out.to_string(),
          Json(hits).stringify(indent=2),
        ),
      )
    }
    "verify" => {
      if args.length() < 3 {
        fail("verify requires a trace file")
      }
      let trace = input_trace(inputs, args[2])
      let contract = contract_by_name(
        required_option(args, "--contract"),
        required_option(args, "--object"),
      )
      let report = contract.check(trace)
      let artifacts : Array[(String, String)] = []
      collect_artifact(
        artifacts,
        counterexample_artifact(
          option(args, "--counterexample", ""),
          report.counterexample,
        ),
      )
      Diagnosis::new(
        exit_code=if report.passed { 0 } else { 2 },
        stdout=debug_result(
          "verify",
          debug_format(args),
          report.report(),
          report.to_json_string(),
        ),
        artifacts~,
      )
    }
    "diverge" => {
      if args.length() < 4 {
        fail("diverge requires expected and actual trace files")
      }
      let expected = input_trace(inputs, args[2])
      let actual = input_trace(inputs, args[3])
      match @frontierlab.first_divergence(expected, actual) {
        Some(divergence) => {
          let counterexample = if divergence.step < actual.steps.length() {
            Some(actual.slice(center=divergence.step))
          } else {
            None
          }
          let artifacts : Array[(String, String)] = []
          collect_artifact(
            artifacts,
            counterexample_artifact(
              option(args, "--counterexample", ""),
              counterexample,
            ),
          )
          Diagnosis::new(
            exit_code=2,
            stdout=debug_result(
              "diverge",
              debug_format(args),
              divergence.message,
              divergence.to_json_string(),
            ),
            artifacts~,
          )
        }
        None =>
          Diagnosis::new(
            stdout=debug_result(
              "diverge",
              debug_format(args),
              "No divergence.",
              "null",
            ),
          )
      }
    }
    other => fail("Unknown command: \{other}\n\{usage()}")
  }
}

///|
fn run_cli(args : Array[String]) -> Int32 raise {
  apply_execution(execute_cli(args, read_cli_inputs(args)))
}