// zxcvbn CLI:对一个密码给出完整的强度评估。
//
// 用法:
// ```bash
// moon run cmd/main -- "Tr0ub4dor&3"
// moon run cmd/main -- --user bob --user bob@example.com "Tr0ub4dor&3"
// moon run cmd/main -- --year 2017 --json "password"
// moon run cmd/main -- --batch "pw1" "pw2" "pw3"
// ```
//
// `--batch` 把所有位置参数都当作密码,逐个评估并输出 JSON Lines(每行一个 JSON
// 对象),供 tools/diff_test.py 差分对拍消费。批内所有密码共用同一组 `--user`
// 与 `--year`,所以需要不同 user_inputs 的用例要分开跑。

///|
fn main {
  run(cli_args()) catch {
    InvalidArguments(message) => {
      eprintln("error: \{message}")
      panic()
    }
  }
}

///|
#cfg(target="js")
fn cli_args() -> Array[String] {
  // Node supplies both the executable and script path; other targets supply one.
  @env.args()[1:].to_owned()
}

///|
#cfg(not(target="js"))
fn cli_args() -> Array[String] {
  @env.args()
}

///|
suberror CliError {
  InvalidArguments(String)
}

///|
/// 解析后的命令行选项。
struct CliOptions {
  /// 位置参数;`--` 之后的所有字符串均原样保留,单密码模式要求恰好一个。
  positionals : Array[String]
  user_inputs : Array[String]
  json : Bool
  /// 批模式:把每个 positionals 元素当一个密码,输出 JSON Lines。
  batch : Bool
  help : Bool
  reference_year : Int32
}

///|
/// 从 `args`(含 argv[0])里解析选项。位置参数先统一收进 `positionals`,
/// 再由 `batch` 决定语义——这样 `--json --user bob "pw"` 与
/// `--batch a b --user bob` 都能正确工作,且选项顺序无关。
fn parse_options(args : Array[String]) -> CliOptions raise CliError {
  let positionals : Array[String] = []
  let user_inputs : Array[String] = []
  let mut json_mode = false
  let mut batch_mode = false
  let mut help = false
  let mut options_done = false
  let mut reference_year = 2026
  let mut i = 1
  while i < args.length() {
    let arg = args[i]
    if options_done {
      positionals.push(arg)
      i += 1
      continue
    }
    match arg {
      "--" => {
        options_done = true
        i += 1
      }
      "--help" => {
        help = true
        i += 1
      }
      "--json" => {
        json_mode = true
        i += 1
      }
      "--batch" => {
        batch_mode = true
        i += 1
      }
      "--user" => {
        i += 1
        if i < args.length() {
          user_inputs.push(args[i])
          i += 1
        } else {
          raise InvalidArguments("--user requires a value")
        }
      }
      "--year" => {
        i += 1
        if i < args.length() {
          reference_year = parse_year_option(args[i])
          i += 1
        } else {
          raise InvalidArguments("--year requires a value")
        }
      }
      _ => {
        if arg.has_prefix("--") {
          raise InvalidArguments(
            "unknown option; put -- before a literal password",
          )
        }
        positionals.push(arg)
        i += 1
      }
    }
  }
  if !help {
    if positionals.is_empty() {
      raise InvalidArguments("a password is required; use --help for usage")
    }
    if !batch_mode && positionals.length() != 1 {
      raise InvalidArguments("multiple passwords require --batch")
    }
  }
  {
    positionals,
    user_inputs,
    json: json_mode,
    batch: batch_mode,
    help,
    reference_year,
  }
}

///|
/// Four ASCII digits plus a range check avoid integer overflow and silent fallback.
fn parse_year_option(s : String) -> Int32 raise CliError {
  let chars = s.to_array()
  if chars.length() != 4 {
    raise InvalidArguments("--year must be a four-digit year in 1900..2100")
  }
  let mut value = 0
  for c in chars {
    if !c.is_ascii_digit() {
      raise InvalidArguments("--year must be a four-digit year in 1900..2100")
    }
    value = value * 10 + (c.to_int() - '0'.to_int())
  }
  if value < 1900 || value > 2100 {
    raise InvalidArguments("--year must be in 1900..2100")
  }
  value
}

///|
fn run(args : Array[String]) -> Unit raise CliError {
  let opts = parse_options(args)
  if opts.help {
    println(
      "usage: moon run cmd/main -- [--user ]... [--year <1900..2100>] [--json] [--batch] [--] ...",
    )
  } else if opts.batch {
    for password in opts.positionals {
      let result = @lib.zxcvbn(
        password,
        opts.user_inputs,
        reference_year=opts.reference_year,
      )
      print_json(result)
    }
  } else {
    let result = @lib.zxcvbn(
      opts.positionals[0],
      opts.user_inputs,
      reference_year=opts.reference_year,
    )
    if opts.json {
      print_json(result)
    } else {
      print_human(result)
    }
  }
}

///|
fn print_human(result : @lib.Entropy) -> Unit {
  println("password      : \{result.password}")
  println("guesses       : \{result.guesses}")
  println("log10(guesses): \{result.guesses_log10}")
  println("score         : \{result.score} / 4")
  let s = result.crack_times_seconds
  println("crack times (seconds):")
  println(
    "  online throttling (100/h)     : \{s.online_throttling_100_per_hour}",
  )
  println(
    "  online no throttling (10/s)   : \{s.online_no_throttling_10_per_second}",
  )
  println(
    "  offline slow hashing (1e4/s)  : \{s.offline_slow_hashing_1e4_per_second}",
  )
  println(
    "  offline fast hashing (1e10/s) : \{s.offline_fast_hashing_1e10_per_second}",
  )
  let d = result.crack_times_display
  println("crack times (display):")
  println(
    "  online throttling (100/h)     : \{d.online_throttling_100_per_hour}",
  )
  println(
    "  online no throttling (10/s)   : \{d.online_no_throttling_10_per_second}",
  )
  println(
    "  offline slow hashing (1e4/s)  : \{d.offline_slow_hashing_1e4_per_second}",
  )
  println(
    "  offline fast hashing (1e10/s) : \{d.offline_fast_hashing_1e10_per_second}",
  )
  println("matches (\{result.sequence.length()}):")
  if result.sequence.length() == 0 {
    println("  (none)")
  } else {
    for m in result.sequence {
      println(
        "  [\{m.start_index()}, \{m.end_index()}] \{m.pattern()} \{m.token()} guesses=\{m.guesses()}",
      )
    }
  }
  println("warning: \{result.feedback.warning}")
  println("suggestions:")
  for suggestion in result.feedback.suggestions {
    println("  - \{suggestion}")
  }
}

///|
/// JSON 输出,供差分对拍(tools/diff_test.py)与 Web 侧消费。
/// 序列化本身在库里(`Entropy::to_json`),这里只负责打印。
fn print_json(result : @lib.Entropy) -> Unit {
  println(result.to_json())
}