///|
fn print_usage() -> Unit {
  println(
    (
      #|MoonRule - explainable rules for JSON data
      #|
      #|Usage:
      #|  moonrule eval  
      #|  moonrule eval-file  
      #|  moonrule explain  
      #|  moonrule explain-file  
      #|  moonrule analyze 
      #|  moonrule analyze-rules 
      #|  moonrule lint 
      #|  moonrule lint-rules 
      #|  moonrule test-rules  
      #|  moonrule check  
      #|  moonrule check-batch  
      #|  moonrule functions
      #|
      #|Examples:
      #|  moonrule eval "user.age >= 18" "{\"user\":{\"age\":21}}"
      #|  moonrule check examples/access-rules.json examples/user.json
      #|,
    ),
  )
}

///|
fn command_index(args : Array[String]) -> Int? {
  for index, value in args {
    if value == "eval" ||
      value == "eval-file" ||
      value == "explain" ||
      value == "explain-file" ||
      value == "analyze" ||
      value == "analyze-rules" ||
      value == "lint" ||
      value == "lint-rules" ||
      value == "test-rules" ||
      value == "check" ||
      value == "check-batch" ||
      value == "functions" {
      return Some(index)
    }
  }
  None
}

///|
fn print_diagnostic(diagnostic : @lib.Diagnostic, source : String) -> Unit {
  println(diagnostic.render(source))
}

///|
fn run_eval(expression : String, json_text : String, explain~ : Bool) -> Int {
  let context = @json.parse(json_text) catch {
    error => {
      println("invalid JSON input: \{error}")
      return 2
    }
  }
  let program = match @lib.compile(expression) {
    Err(diagnostic) => {
      print_diagnostic(diagnostic, expression)
      return 2
    }
    Ok(program) => program
  }
  if explain {
    match @lib.explain(program, context) {
      Err(diagnostic) => {
        print_diagnostic(diagnostic, expression)
        2
      }
      Ok(trace) => {
        println(trace.to_json_string())
        0
      }
    }
  } else {
    match @lib.evaluate(program, context) {
      Err(diagnostic) => {
        print_diagnostic(diagnostic, expression)
        2
      }
      Ok(value) => {
        println(value.stringify(indent=2))
        0
      }
    }
  }
}

///|
fn run_eval_file(expression : String, path : String, explain~ : Bool) -> Int {
  let json_text = @fs.read_file_to_string(path) catch {
    _ => {
      println("unable to read data file: \{path}")
      return 2
    }
  }
  run_eval(expression, json_text, explain~)
}

///|
fn load_rules(path : String) -> Result[@lib.RuleSet, Int] {
  let rules_text = @fs.read_file_to_string(path) catch {
    _ => {
      println("unable to read rules file: \{path}")
      return Err(2)
    }
  }
  let rules_json = @json.parse(rules_text) catch {
    error => {
      println("invalid rules JSON: \{error}")
      return Err(2)
    }
  }
  match @lib.RuleSet::from_json(rules_json) {
    Err(diagnostics) => {
      for diagnostic in diagnostics {
        print_diagnostic(diagnostic, rules_text)
      }
      Err(2)
    }
    Ok(rules) => Ok(rules)
  }
}

///|
fn run_check(rules_path : String, data_path : String) -> Int {
  let rules = match load_rules(rules_path) {
    Err(code) => return code
    Ok(rules) => rules
  }
  let data_text = @fs.read_file_to_string(data_path) catch {
    _ => {
      println("unable to read data file: \{data_path}")
      return 2
    }
  }
  let data_json = @json.parse(data_text) catch {
    error => {
      println("invalid data JSON: \{error}")
      return 2
    }
  }
  let report = rules.evaluate(data_json)
  println(report.to_json_string())
  if report.passed {
    0
  } else {
    1
  }
}

///|
fn run_check_batch(rules_path : String, data_path : String) -> Int {
  let rules = match load_rules(rules_path) {
    Err(code) => return code
    Ok(rules) => rules
  }
  let data_text = @fs.read_file_to_string(data_path) catch {
    _ => {
      println("unable to read data file: \{data_path}")
      return 2
    }
  }
  let data_json = @json.parse(data_text) catch {
    error => {
      println("invalid data JSON: \{error}")
      return 2
    }
  }
  let contexts : Array[Json] = @json.from_json(data_json) catch {
    error => {
      println("batch input must be a JSON array: \{error}")
      return 2
    }
  }
  let report = rules.evaluate_batch(contexts)
  println(report.to_json_string())
  if report.passed {
    0
  } else {
    1
  }
}

///|
fn run_analyze(expression : String) -> Int {
  match @lib.compile(expression) {
    Err(diagnostic) => {
      print_diagnostic(diagnostic, expression)
      2
    }
    Ok(program) => {
      let analysis = @lib.analyze(program)
      println(analysis.to_json_string())
      if analysis.warning_count() == 0 {
        0
      } else {
        1
      }
    }
  }
}

///|
fn run_analyze_rules(rules_path : String) -> Int {
  match load_rules(rules_path) {
    Err(code) => code
    Ok(rules) => {
      let analysis = rules.analyze()
      println(analysis.to_json_string())
      if analysis.warning_count == 0 {
        0
      } else {
        1
      }
    }
  }
}

///|
fn run_lint(expression : String) -> Int {
  match @lib.compile(expression) {
    Err(diagnostic) => {
      print_diagnostic(diagnostic, expression)
      2
    }
    Ok(program) =>
      match @lib.analyze_with_policy(program, @lib.AnalysisPolicy::default()) {
        Err(diagnostic) => {
          print_diagnostic(diagnostic, expression)
          2
        }
        Ok(report) => {
          println(report.to_json_string())
          if report.passed {
            0
          } else {
            1
          }
        }
      }
  }
}

///|
fn run_lint_rules(rules_path : String) -> Int {
  match load_rules(rules_path) {
    Err(code) => code
    Ok(rules) =>
      match rules.analyze_with_policy(@lib.AnalysisPolicy::default()) {
        Err(diagnostic) => {
          print_diagnostic(diagnostic, "")
          2
        }
        Ok(report) => {
          println(report.to_json_string())
          if report.passed {
            0
          } else {
            1
          }
        }
      }
  }
}

///|
fn load_test_suite(path : String) -> Result[@lib.RuleTestSuite, Int] {
  let suite_text = @fs.read_file_to_string(path) catch {
    _ => {
      println("unable to read rule test suite: \{path}")
      return Err(2)
    }
  }
  let suite_json = @json.parse(suite_text) catch {
    error => {
      println("invalid rule test suite JSON: \{error}")
      return Err(2)
    }
  }
  match @lib.RuleTestSuite::from_json(suite_json) {
    Err(diagnostics) => {
      for diagnostic in diagnostics {
        print_diagnostic(diagnostic, suite_text)
      }
      Err(2)
    }
    Ok(suite) => Ok(suite)
  }
}

///|
fn run_test_rules(rules_path : String, cases_path : String) -> Int {
  let rules = match load_rules(rules_path) {
    Err(code) => return code
    Ok(rules) => rules
  }
  let suite = match load_test_suite(cases_path) {
    Err(code) => return code
    Ok(suite) => suite
  }
  let report = rules.run_test_suite(suite)
  println(report.to_json_string())
  if report.passed {
    0
  } else {
    1
  }
}

///|
fn main {
  let args = @sys.get_cli_args()
  guard command_index(args) is Some(index) else {
    print_usage()
    return
  }
  let remaining = args.length() - index - 1
  let exit_code = match args[index] {
    "eval" if remaining == 2 =>
      run_eval(args[index + 1], args[index + 2], explain=false)
    "eval-file" if remaining == 2 =>
      run_eval_file(args[index + 1], args[index + 2], explain=false)
    "explain" if remaining == 2 =>
      run_eval(args[index + 1], args[index + 2], explain=true)
    "explain-file" if remaining == 2 =>
      run_eval_file(args[index + 1], args[index + 2], explain=true)
    "analyze" if remaining == 1 => run_analyze(args[index + 1])
    "analyze-rules" if remaining == 1 => run_analyze_rules(args[index + 1])
    "lint" if remaining == 1 => run_lint(args[index + 1])
    "lint-rules" if remaining == 1 => run_lint_rules(args[index + 1])
    "test-rules" if remaining == 2 =>
      run_test_rules(args[index + 1], args[index + 2])
    "check" if remaining == 2 => run_check(args[index + 1], args[index + 2])
    "check-batch" if remaining == 2 =>
      run_check_batch(args[index + 1], args[index + 2])
    "functions" if remaining == 0 => {
      println(@lib.builtin_functions_json())
      0
    }
    _ => {
      print_usage()
      2
    }
  }
  @sys.exit(exit_code)
}