///|
/// `wxlogin` — the server-side half of the mini-program login flow that
/// WeChat Pay JSAPI checkout needs.
///
/// `wx.login` codes are minted inside the WeChat client only (a real
/// device or WeChat DevTools); no server API can create one from appid and
/// secret. Give this command a fresh code plus the mini-program's
/// credentials, and it performs the exchange (`GET /sns/jscode2session`)
/// and prints the payer `openid` a JSAPI order needs:
///
///     moon run cmd/wxlogin -- --appid wx1234567890abcdef --secret SECRET --code 081Ab2Cd3Ef4Gh5
///
/// Missing values fall back to `WXPAY_APPID` / `WXPAY_APP_SECRET` (the
/// same variables cmd/webdemo uses) and finally to an interactive prompt.
///
/// With `--project ` the command instead drives WeChat DevTools over
/// its automation port, runs `wx.login` inside the real project and just
/// prints the fresh code — hand it to the webdemo mini-program panel,
/// which exchanges it for the openid and places the order:
///
///     moon run cmd/wxlogin -- --project ~/mini-programs/demo
async fn main {
  let matches = @argparse.Command(
    "wxlogin",
    about="Exchange a wx.login code for the openid used in WeChat mini-program payment (code2session)",
    options=[
      OptionArg(
        "appid",
        long="appid",
        about="mini-program AppID (falls back to $WXPAY_APPID, then a prompt)",
      ),
      OptionArg(
        "secret",
        long="secret",
        about="mini-program AppSecret (falls back to $WXPAY_APP_SECRET, then a prompt)",
      ),
      OptionArg(
        "code",
        long="code",
        about="fresh js_code from wx.login (single use, expires in 5 minutes)",
      ),
      OptionArg(
        "project",
        long="project",
        about="mini-program project dir: drive WeChat DevTools to grab a real wx.login code automatically",
      ),
      OptionArg(
        "cli",
        long="cli",
        about="WeChat DevTools cli binary (falls back to $WXPAY_DEVTOOLS_CLI, then the macOS default)",
      ),
    ],
  ).parse()
  let project = match matches.values {
    { "project": [value], .. } => Some(value)
    _ => None
  }
  let cli_flag = match matches.values {
    { "cli": [value], .. } => Some(value)
    _ => None
  }
  match project {
    Some(project) if !project.is_empty() =>
      return run_auto_mode(cli_flag, project)
    _ => ()
  }
  let appid_flag = match matches.values {
    { "appid": [value], .. } => Some(value)
    _ => None
  }
  let secret_flag = match matches.values {
    { "secret": [value], .. } => Some(value)
    _ => None
  }
  let code_flag = match matches.values {
    { "code": [value], .. } => Some(value)
    _ => None
  }
  let appid = resolve(appid_flag, "WXPAY_APPID", "mini-program AppID")
  let secret = resolve(
    secret_flag, "WXPAY_APP_SECRET", "mini-program AppSecret",
  )
  let js_code = match code_flag {
    Some(code) if !code.is_empty() => code
    _ => {
      print_how_to_get_code()
      resolve(None, "", "wx.login code")
    }
  }
  let config = @miniprogram.Config::new(app_id=appid, app_secret=secret)
  let session = try
    @miniprogram.code2session_with(
      config~,
      transport=@transport.HttpClient::new(),
      js_code~,
    )
  catch {
    Api(api) => {
      println("")
      println("WeChat rejected the exchange: \{api.code} — \{api.message}")
      println("hint: \{errcode_hint(api.code)}")
      fail("wxlogin aborted")
    }
    e => {
      println("")
      println("code2session failed: \{e}")
      fail("wxlogin aborted")
    }
  } noraise {
    session => session
  }
  print_session(session)
}

///|
/// First non-empty source wins: explicit flag, then the environment, then
/// an interactive prompt reading one line from stdin.
async fn resolve(flag : String?, env_name : String, label : String) -> String {
  match flag {
    Some(value) if !value.is_empty() => return value
    _ => ()
  }
  if env_name != "" {
    match @env.get_env_var(env_name) {
      Some(value) if !value.is_empty() => return value
      _ => ()
    }
  }
  println("\{label} (paste and press Enter):")
  match @stdio.stdin.read_until("\n") {
    Some(line) =>
      match line.trim().to_owned() {
        "" => fail("\{label} is required")
        value => value
      }
    None => fail("\{label} is required (stdin closed)")
  }
}

///|
/// The code cannot be fetched by any server-side API — it is minted inside
/// the WeChat client. Point the developer at the fastest ways to get one.
fn print_how_to_get_code() -> Unit {
  println("")
  println("A wx.login code can only be produced by the WeChat client —")
  println("no server-side API mints one from appid/secret. Fastest paths:")
  println(
    "  1. WeChat DevTools console:  wx.login().then(res => console.log(res.code))",
  )
  println("  2. Or call wx.login() inside your mini program and log res.code.")
  println("Codes are single-use and expire within 5 minutes.")
  println("Re-run with --code , or paste one below.")
}

///|
/// Print the exchange result: everything a JSAPI order needs.
fn print_session(session : @miniprogram.Session) -> Unit {
  println("")
  println("code2session ok — payer identity for JSAPI payment:")
  println("  openid      : \{session.openid()}")
  println(
    "  session_key : \{session.session_key()}  (keep server-side; payment only needs openid)",
  )
  match session.unionid() {
    Some(unionid) => println("  unionid     : \{unionid}")
    None =>
      println(
        "  unionid     : (absent — app not bound to an open-platform account)",
      )
  }
  println("")
  println("next: place a JSAPI order with this openid, e.g.")
  println(
    "  client.pay_with_openid(out_trade_no=..., total=..., description=..., notify_url=..., openid=\"\{session.openid()}\")",
  )
}

///|
/// A one-line explanation for the login error codes seen in practice.
fn errcode_hint(code : String) -> String {
  match code {
    "40029" =>
      "the code is invalid, already used, or expired — grab a fresh one from wx.login"
    "40013" => "the appid is invalid"
    "41004" | "40125" => "the appsecret is missing or wrong"
    "45011" => "rate limited by WeChat — wait a minute and retry"
    "-1" => "WeChat system busy — retry shortly"
    _ => "see WeChat's global return code list"
  }
}

///|
/// Default DevTools cli location (matches what miniprogram-automator
/// resolves: the Windows cli.bat when present, else the macOS bundle).
async fn default_cli_path() -> String {
  let windows = "C:/Program Files (x86)/Tencent/微信web开发者工具/cli.bat"
  if @fs.exists(windows) {
    windows
  } else {
    "/Applications/wechatwebdevtools.app/Contents/MacOS/cli"
  }
}

///|
/// Automation mode: launch WeChat DevTools with its automation port,
/// run `wx.login` inside the real project and print the fresh code. No
/// appid/secret needed here — webdemo does the openid exchange and the
/// order with this code.
async fn run_auto_mode(cli_flag : String?, project_path : String) -> Unit {
  let cli_path = match cli_flag {
    Some(path) if !path.is_empty() => path
    _ =>
      match @env.get_env_var("WXPAY_DEVTOOLS_CLI") {
        Some(path) if !path.is_empty() => path
        _ => default_cli_path()
      }
  }
  if !@fs.exists(cli_path) {
    fail(
      "DevTools cli not found at \{cli_path} — pass --cli  or set WXPAY_DEVTOOLS_CLI",
    )
  }
  if !@fs.exists(project_path) {
    fail("mini-program project not found: \{project_path}")
  }
  println(
    "fetching a fresh wx.login code from the DevTools project \{project_path}",
  )
  launch_devtools(cli_path~, project_path~, port=DEFAULT_AUTO_PORT)
  let automator = Automator::connect("ws://127.0.0.1:\{DEFAULT_AUTO_PORT}")
  let code = try
    @async.with_timeout(
      60_000,
      () => grab_code(automator),
      error=Timeout("wx.login via DevTools timed out after 60s"),
    )
  catch {
    e => {
      automator.close()
      println("")
      println("automation failed: \{e}")
      fail("wxlogin aborted")
    }
  } noraise {
    code => {
      automator.close()
      code
    }
  }
  print_code(code, project_path)
}

///|
/// Liveness probe, then the login call itself.
async fn grab_code(automator : Automator) -> String {
  let _ = automator.get_info()
  let result = automator.call_wx_method("login")
  login_code_from(result)
}

///|
/// The automation port miniprogram-automator uses by default.
const DEFAULT_AUTO_PORT : Int32 = 9420

///|
/// Print the grabbed code plus the fastest way to turn it into an order.
fn print_code(code : String, project_path : String) -> Unit {
  println("")
  println("wx.login ok — fresh code from the DevTools project:")
  println("  project : \{project_path}")
  println("  code    : \{code}")
  println("")
  println("next: paste it into the webdemo mini-program panel, or POST it:")
  println(
    "  curl -X POST 'http://127.0.0.1:1943/api/wechat/miniapp/orders?amount_yuan=0.01&js_code=\{code}'",
  )
  println(
    "(single use, expires in 5 minutes; the IDE stays open — rerun for another code)",
  )
}