///|
pub(all) enum Command {
  Build
  Doctor
  Check
  Inspect
  Plan
  Verify
  Init
  Har
  Help
  Version
} derive(Eq, Debug)

///|
pub struct CliOptions {
  command : Command
  config : String
  sdk : String?
  abis : Array[String]
  output : String
  json : Bool
  dry_run : Bool
  cache : Bool
  /// `init`: project directory, module name, package and platform module path.
  target : String?
  name : String?
  package_name : String?
  ohos : String?
  /// `har`: verified dist, HAR module directory and optional hvigor assembly.
  dist : String?
  module_dir : String?
  deveco : String?
  assemble : Bool
}

///|
pub(all) struct Diagnostic {
  stage : String
  code : String
  message : String
  context : Array[String]
}

///|
pub fn Diagnostic::to_json(self : Diagnostic) -> Json {
  {
    "stage": self.stage,
    "code": self.code,
    "message": self.message,
    "context": self.context.to_json(),
  }
}

///|
pub fn Diagnostic::render(self : Diagnostic) -> String {
  let prefix = self.stage + " [" + self.code + "]: " + self.message
  if self.context.is_empty() {
    prefix
  } else {
    prefix + "\n  " + self.context.join("\n  ")
  }
}

///|
pub fn command_name(command : Command) -> String {
  match command {
    Build => "build"
    Doctor => "doctor"
    Check => "check"
    Inspect => "inspect"
    Plan => "plan"
    Verify => "verify"
    Init => "init"
    Har => "har"
    Help => "help"
    Version => "version"
  }
}

///|
fn parse_command(text : String) -> Command raise ConfigError {
  match text {
    "build" => Build
    "doctor" => Doctor
    "check" => Check
    "inspect" => Inspect
    "plan" => Plan
    "verify" => Verify
    "init" => Init
    "har" => Har
    "--help" | "-h" => Help
    "--version" => Version
    _ => raise Invalid("unknown command: " + text)
  }
}

///|
pub fn parse_abis(text : String) -> Array[String] raise ConfigError {
  let result : Array[String] = []
  for part in text.split(",") {
    let abi = part.to_owned()
    if abi != "arm64-v8a" && abi != "x86_64" {
      raise Invalid("unsupported ABI: " + abi)
    }
    if result.contains(abi) {
      raise Invalid("duplicate ABI: " + abi)
    }
    result.push(abi)
  }
  if result.is_empty() {
    raise Invalid("at least one ABI is required")
  }
  result
}

///|
fn allowed_option(command : Command, option : String) -> Bool {
  match option {
    "--json" => command != Help && command != Version
    "--config" =>
      command != Verify &&
      command != Help &&
      command != Version &&
      command != Init &&
      command != Har
    "--sdk" => command == Build || command == Doctor || command == Plan
    "--abi" => command == Build || command == Plan
    "--out" => command == Build || command == Verify || command == Har
    "--dry-run" | "--cache" => command == Build
    "--name" | "--package" | "--ohos" => command == Init
    "--dist" | "--module" | "--deveco" | "--assemble" => command == Har
    _ => false
  }
}

///|
/// Arguments exclude the executable name. Paths are resolved by the command.
pub fn parse_cli(args : Array[String]) -> CliOptions raise ConfigError {
  let command = if args.is_empty() { Help } else { parse_command(args[0]) }
  let mut config = "moonohos.json"
  let mut sdk : String? = None
  let mut abis = ["arm64-v8a", "x86_64"]
  let mut output = "_build/moonohos/dist"
  let mut json = false
  let mut dry_run = false
  let mut cache = false
  let mut target : String? = None
  let mut name : String? = None
  let mut package_name : String? = None
  let mut ohos : String? = None
  let mut dist : String? = None
  let mut module_dir : String? = None
  let mut deveco : String? = None
  let mut assemble = false
  let seen : Map[String, Bool] = Map([])
  let mut index = 1
  if command == Init && args.length() > 1 && !args[1].has_prefix("--") {
    target = Some(args[1])
    index = 2
  }
  while index < args.length() {
    let option = args[index]
    if option == "--help" || option == "-h" {
      return {
        command: Help,
        config,
        sdk,
        abis,
        output,
        json,
        dry_run,
        cache,
        target,
        name,
        package_name,
        ohos,
        dist,
        module_dir,
        deveco,
        assemble,
      }
    }
    if !allowed_option(command, option) {
      raise Invalid(
        "option " + option + " is not supported by " + command_name(command),
      )
    }
    if seen.contains(option) {
      raise Invalid("duplicate option: " + option)
    }
    seen[option] = true
    match option {
      "--json" => json = true
      "--dry-run" => dry_run = true
      "--cache" => cache = true
      "--assemble" => assemble = true
      _ => {
        if index + 1 >= args.length() || args[index + 1].has_prefix("--") {
          raise Invalid("missing value for " + option)
        }
        index += 1
        let value = args[index]
        if value == "" {
          raise Invalid("empty value for " + option)
        }
        match option {
          "--config" => config = value
          "--sdk" => sdk = Some(value)
          "--abi" => abis = parse_abis(value)
          "--out" => output = value
          "--name" => name = Some(value)
          "--package" => package_name = Some(value)
          "--ohos" => ohos = Some(value)
          "--dist" => dist = Some(value)
          "--module" => module_dir = Some(value)
          "--deveco" => deveco = Some(value)
          _ => raise Invalid("unknown option: " + option)
        }
      }
    }
    index += 1
  }
  if command == Build && sdk is None {
    raise Invalid("build requires --sdk ")
  }
  if command == Init && target is None {
    raise Invalid("init requires a project directory: moonohos init ")
  }
  if command == Har && (dist is None || module_dir is None) {
    raise Invalid("har requires --dist  and --module ")
  }
  if assemble && deveco is None {
    raise Invalid("--assemble requires --deveco ")
  }
  {
    command,
    config,
    sdk,
    abis,
    output,
    json,
    dry_run,
    cache,
    target,
    name,
    package_name,
    ohos,
    dist,
    module_dir,
    deveco,
    assemble,
  }
}

///|
pub fn success_report(command : String, result : Json) -> Json {
  { "command": command, "ok": true, "exitCode": 0, "result": result }
}

///|
pub fn failure_report(
  command : String,
  diagnostic : Diagnostic,
  parameter? : Bool = false,
) -> Json {
  {
    "command": command,
    "ok": false,
    "exitCode": if parameter {
      2
    } else {
      3
    },
    "diagnostics": [diagnostic.to_json()].to_json(),
  }
}

///|
#deprecated
pub extend Command with Eq::{equal, not_equal}

///|
#deprecated
pub extend Command with @debug.Debug::{to_repr}