///|
/// A minimal web app for manually integration-testing moon-pay-sdk's WeChat
/// Pay Native (QR) checkout and Alipay page (desktop website) checkout
/// against the real gateways. It serves one page per gateway:
///
/// - `/` (WeChat): create an order, render the returned `code_url` as a QR
///   code and poll until WeChat Pay reports the order paid.
/// - `/alipay`: create a page-pay order (`alipay.trade.page.pay`) and
///   redirect the browser to the returned cashier URL; when Alipay redirects
///   back to `/alipay?alipay_order=` the page resumes polling.
///
/// Each page is enabled only when its gateway's credentials are present, so
/// the app runs with either gateway alone. Credentials come from WXPAY_* /
/// ALIPAY_* environment variables so none of them live in this repository.
///
/// Run from the repository root:
///
///     moon run cmd/webdemo
///
/// then open  (WeChat) or
///  (Alipay).

///|
/// One order this app has placed, keyed by out_trade_no.
priv struct OrderRec {
  amount_fen : Int32
  mut notify_seen : Bool
  /// Placed under the mini-program panel's own merchant identity — poll
  /// it through app.miniapp_pay instead of the main WeChat client.
  miniapp : Bool
}

///|
priv struct App {
  wechat : @wechat.Client?
  notify_url : String
  /// The mini-program's own login credentials — independent of the pay
  /// appid so the code exchange can target a different mini program.
  miniapp_config : @miniprogram.Config?
  /// Orders for the mini-program panel, placed under the mini program's
  /// own appid; only built when that differs from the pay appid.
  miniapp_pay : @wechat.Client?
  alipay : @alipay.Client?
  alipay_notify_url : String
  orders : Map[String, OrderRec]
  mut seq : Int32
}

///|
const LISTEN_PORT : Int32 = 1943

///|
async fn main {
  let wechat = load_wechat()
  let alipay = load_alipay()
  if wechat is None && alipay is None {
    return
  }
  if wechat is None {
    println("note: WeChat Pay page disabled (WXPAY_* incomplete)")
  }
  if alipay is None {
    println("note: Alipay page disabled (ALIPAY_* incomplete)")
  }
  let (wechat_client, notify_url) = match wechat {
    Some((client, notify_url)) => (Some(client), notify_url)
    None => (None, "")
  }
  // The mini program's own credentials: WXPAY_MINIAPP_APPID /
  // WXPAY_MINIAPP_APP_SECRET win; both fall back to the pay appid and
  // WXPAY_APP_SECRET so the common single-mini-program setup stays a
  // one-variable configuration.
  let miniapp_appid = match @env.get_env_var("WXPAY_MINIAPP_APPID") {
    Some(appid) if appid != "" => Some(appid)
    _ =>
      match wechat_client {
        Some(pay) => Some(pay.config.appid)
        None => None
      }
  }
  let miniapp_secret = match @env.get_env_var("WXPAY_MINIAPP_APP_SECRET") {
    Some(secret) if secret != "" => Some(secret)
    _ =>
      match @env.get_env_var("WXPAY_APP_SECRET") {
        Some(secret) if secret != "" => Some(secret)
        _ => None
      }
  }
  let miniapp_config : @miniprogram.Config? = match
    (miniapp_appid, miniapp_secret) {
    (Some(app_id), Some(app_secret)) =>
      Some(@miniprogram.Config::new(app_id~, app_secret~))
    _ => None
  }
  // The mini-program panel's own merchant identity: every merchant field
  // can be overridden with a WXPAY_MINIAPP_* variable and otherwise falls
  // back to the pay-side value — a mini program bound to another merchant
  // account works by setting only what differs.
  let miniapp_mchid = miniapp_env("WXPAY_MINIAPP_MCHID", "WXPAY_MCHID")
  let miniapp_serial_no = miniapp_env(
    "WXPAY_MINIAPP_SERIAL_NO", "WXPAY_SERIAL_NO",
  )
  let miniapp_api_v3_key = miniapp_env(
    "WXPAY_MINIAPP_API_V3_KEY", "WXPAY_API_V3_KEY",
  )
  let miniapp_key_pem = match load_pem_env("WXPAY_MINIAPP_PRIVATE_KEY", []) {
    Some(pem) => Some(pem)
    None => load_pem_env("WXPAY_PRIVATE_KEY", [])
  }
  let miniapp_base_url = match @env.get_env_var("WXPAY_BASE_URL") {
    Some(url) if url != "" => url
    _ => "https://api.mch.weixin.qq.com"
  }
  let miniapp_pub_pem = miniapp_env(
    "WXPAY_MINIAPP_PUBLIC_KEY", "WXPAY_PUBLIC_KEY",
  )
  let miniapp_pub_id = miniapp_env(
    "WXPAY_MINIAPP_PUBLIC_KEY_ID", "WXPAY_PUBLIC_KEY_ID",
  )
  let miniapp_pay : @wechat.Client? = match
    (
      miniapp_config, miniapp_mchid, miniapp_serial_no, miniapp_api_v3_key, miniapp_key_pem,
    ) {
    (
      Some(config),
      Some(mchid),
      Some(serial_no),
      Some(api_v3_key),
      Some(private_key_pem),
    ) => {
      let pub_pem = match (miniapp_pub_pem, miniapp_pub_id) {
        (Some(pem), Some(id)) if pem != "" && id != "" => Some((pem, id))
        _ => None
      }
      let (pub_pem, pub_id) = match pub_pem {
        Some((pem, id)) => (pem, id)
        None => ("", "")
      }
      try
        Some(
          @wechat.Client::new(
            config=@wechat.Config::new(
              appid=config.app_id,
              mchid~,
              serial_no~,
              private_key_pem~,
              api_v3_key~,
              base_url=miniapp_base_url,
              platform_public_key_pem=pub_pem,
              platform_public_key_id=pub_id,
            ),
            transport=@transport.HttpClient::new(),
          ),
        )
      catch {
        e => {
          println("miniapp pay client construction failed: \{e}")
          None
        }
      } noraise {
        client => client
      }
    }
    _ => None
  }
  match (miniapp_config, miniapp_mchid, wechat_client) {
    (Some(config), Some(mchid), Some(pay)) =>
      if config.app_id != pay.config.appid || mchid != pay.config.mchid {
        println(
          "note: mini-program panel runs under appid \{config.app_id} / mchid \{mchid} (pay side: \{pay.config.appid} / \{pay.config.mchid})",
        )
      }
    _ => ()
  }
  if wechat_client is Some(_) && miniapp_config is None {
    println(
      "note: mini-program pay disabled (set WXPAY_APP_SECRET or WXPAY_MINIAPP_APP_SECRET to enable it)",
    )
  }
  let (alipay_client, alipay_notify_url) = match alipay {
    Some((client, notify_url)) => (Some(client), notify_url)
    None => (None, "")
  }
  let app : App = {
    wechat: wechat_client,
    notify_url,
    miniapp_config,
    miniapp_pay,
    alipay: alipay_client,
    alipay_notify_url,
    orders: Map([]),
    seq: 0,
  }
  let index_html = read_asset("cmd/webdemo/assets/index.html")
  let alipay_html = read_asset("cmd/webdemo/assets/alipay.html")
  let qrcode_js = read_asset("cmd/webdemo/assets/qrcode.js")
  let server = @http.Server::Server(
    @socket.Addr::parse("0.0.0.0:\{LISTEN_PORT}") catch {
      _ => @socket.Addr::new(0U, LISTEN_PORT)
    },
  )
  let port = server.addr.port()
  println("listening on http://127.0.0.1:\{port} — open it in a browser")
  server.run_forever(
    async fn(req, reader, conn) {
      route(app, index_html, alipay_html, qrcode_js, req, reader, conn)
    },
    allow_failure=true,
  ) catch {
    e => println("server stopped: \{e}")
  }
}

///|
/// Read the WXPAY_* credentials from the environment, print what is missing,
/// and build the WeChat Pay client. Returns None when the page stays
/// disabled.
async fn load_wechat() -> (@wechat.Client, String)? {
  let missing : Array[String] = []
  let appid = require_env("WXPAY_APPID", missing)
  let mchid = require_env("WXPAY_MCHID", missing)
  let serial_no = require_env("WXPAY_SERIAL_NO", missing)
  let api_v3_key = require_env("WXPAY_API_V3_KEY", missing)
  let private_key_pem = load_pem_env("WXPAY_PRIVATE_KEY", missing)
  let base_url = match @env.get_env_var("WXPAY_BASE_URL") {
    Some(url) => url
    None => "https://api.mch.weixin.qq.com"
  }
  let notify_url = match @env.get_env_var("WXPAY_NOTIFY_URL") {
    Some(url) => url
    None => "https://example.com/moon-pay-sdk/wechat/notify"
  }
  // Public-key mode (公钥模式): the merchant-console WeChat Pay public key
  // and its PUB_KEY_ID_... serial, both optional (certificate mode omits them).
  let pubkey : (String, String)? = match
    (
      @env.get_env_var("WXPAY_PUBLIC_KEY"),
      @env.get_env_var("WXPAY_PUBLIC_KEY_ID"),
    ) {
    (Some(raw), Some(id)) =>
      if id == "" {
        missing.push("WXPAY_PUBLIC_KEY_ID (set but empty)")
        None
      } else if raw == "" {
        missing.push("WXPAY_PUBLIC_KEY (set but empty)")
        None
      } else if raw.has_prefix("-----BEGIN") {
        Some((raw, id))
      } else {
        match read_file_text(raw) {
          Some(pem) => Some((pem, id))
          None => {
            missing.push(
              "WXPAY_PUBLIC_KEY (not a PEM and not a readable file: \{raw})",
            )
            None
          }
        }
      }
    (Some(_), None) => {
      missing.push(
        "WXPAY_PUBLIC_KEY_ID (required when WXPAY_PUBLIC_KEY is set)",
      )
      None
    }
    (None, Some(_)) => {
      missing.push(
        "WXPAY_PUBLIC_KEY (required when WXPAY_PUBLIC_KEY_ID is set)",
      )
      None
    }
    (None, None) => None
  }
  if missing.length() > 0 ||
    appid is None ||
    mchid is None ||
    serial_no is None ||
    api_v3_key is None ||
    private_key_pem is None {
    println("webdemo needs WeChat Pay merchant credentials; set:")
    for name in missing {
      println("  \{name}")
    }
    println(
      "WXPAY_APPID / WXPAY_MCHID / WXPAY_SERIAL_NO / WXPAY_API_V3_KEY take the merchant",
    )
    println(
      "values from the WeChat Pay merchant console; WXPAY_PRIVATE_KEY is either the",
    )
    println(
      "API-certificate private key in PEM or a path to the PEM file. Optional:",
    )
    println("WXPAY_BASE_URL (default production), WXPAY_NOTIFY_URL.")
    println(
      "Optional public-key mode (公钥模式): WXPAY_PUBLIC_KEY is the console's",
    )
    println(
      "pub_key.pem (PEM text or file path) and WXPAY_PUBLIC_KEY_ID its serial,",
    )
    println("e.g. PUB_KEY_ID_25566888 — set both or neither.")
    println(
      "Note: variables must be exported (export WXPAY_...) — a shell variable",
    )
    println(
      "without `export` is invisible to the webdemo, and a sourced file must",
    )
    println("be re-sourced after editing.")
    return None
  }
  let (pub_pem, pub_id) = match pubkey {
    Some((pem, id)) => (pem, id)
    None => ("", "")
  }
  let config = @wechat.Config::new(
    appid=appid.unwrap(),
    mchid=mchid.unwrap(),
    serial_no=serial_no.unwrap(),
    private_key_pem=private_key_pem.unwrap(),
    api_v3_key=api_v3_key.unwrap(),
    base_url~,
    platform_public_key_pem=pub_pem,
    platform_public_key_id=pub_id,
  )
  let client = @wechat.Client::new(
    config~,
    transport=@transport.HttpClient::new(),
  ) catch {
    e => {
      println("wechat client construction failed: \{e}")
      return None
    }
  }
  println("appid      : \{appid.unwrap()}")
  println("mchid      : \{mchid.unwrap()}")
  println("base_url   : \{base_url}")
  println("notify_url : \{notify_url}")
  if pub_pem == "" {
    println("verify mode: platform certificates (/v3/certificates)")
  } else {
    println("verify mode: public-key mode (\{pub_id})")
  }
  Some((client, notify_url))
}

///|
/// Read the ALIPAY_* credentials the same way as the WeChat ones and build
/// the Alipay client. Returns None when the page stays disabled.
async fn load_alipay() -> (@alipay.Client, String)? {
  let missing : Array[String] = []
  let appid = require_env("ALIPAY_APPID", missing)
  let private_key_pem = load_pem_env("ALIPAY_PRIVATE_KEY", missing)
  let alipay_public_key_pem = load_pem_env("ALIPAY_PUBLIC_KEY", missing)
  let gateway_url = match @env.get_env_var("ALIPAY_GATEWAY_URL") {
    Some(url) => url
    None => "https://openapi.alipay.com/gateway.do"
  }
  let notify_url = match @env.get_env_var("ALIPAY_NOTIFY_URL") {
    Some(url) => url
    None => "https://example.com/moon-pay-sdk/alipay/notify"
  }
  if missing.length() > 0 ||
    appid is None ||
    private_key_pem is None ||
    alipay_public_key_pem is None {
    println("Alipay page disabled; to enable it set:")
    for name in missing {
      println("  \{name}")
    }
    println("ALIPAY_APPID is the open-platform web-application id (2021...),")
    println(
      "ALIPAY_PRIVATE_KEY the application private key in PEM (text or file path),",
    )
    println(
      "ALIPAY_PUBLIC_KEY the Alipay public key shown in the open-platform console",
    )
    println(
      "(公钥模式) — not your own application public key; PEM text or file path.",
    )
    println(
      "Optional: ALIPAY_GATEWAY_URL (default production), ALIPAY_NOTIFY_URL.",
    )
    println(
      "Note: variables must be exported (export ALIPAY_...) — a shell variable",
    )
    println(
      "without `export` is invisible to the webdemo, and a sourced file must",
    )
    println("be re-sourced after editing.")
    return None
  }
  let config = @alipay.Config::new(
    app_id=appid.unwrap(),
    private_key_pem=private_key_pem.unwrap(),
    alipay_public_key_pem=alipay_public_key_pem.unwrap(),
    gateway_url~,
  )
  let client = @alipay.Client::new(
    config~,
    transport=@transport.HttpClient::new(),
  ) catch {
    e => {
      println("alipay client construction failed: \{e}")
      return None
    }
  }
  println("alipay appid      : \{appid.unwrap()}")
  println("alipay gateway    : \{gateway_url}")
  println("alipay notify_url : \{notify_url}")
  Some((client, notify_url))
}

///|
/// The mini-program override of one merchant variable: a non-empty
/// WXPAY_MINIAPP_* value wins, otherwise the pay-side variable applies.
fn miniapp_env(mini_name : String, pay_name : String) -> String? {
  match @env.get_env_var(mini_name) {
    Some(value) if value != "" => Some(value)
    _ => @env.get_env_var(pay_name)
  }
}

///|
/// Read a required variable; the missing list distinguishes an absent name
/// from one exported with an empty value, and both leave `None` behind.
fn require_env(name : String, missing : Array[String]) -> String? {
  match @env.get_env_var(name) {
    Some(value) =>
      if value == "" {
        missing.push("\{name} (set but empty)")
        None
      } else {
        Some(value)
      }
    None => {
      missing.push(name)
      None
    }
  }
}

///|
/// Read a PEM-valued variable: inline PEM text, or a path to a PEM file.
async fn load_pem_env(name : String, missing : Array[String]) -> String? {
  match @env.get_env_var(name) {
    Some(raw) =>
      if raw == "" {
        missing.push("\{name} (set but empty)")
        None
      } else if raw.has_prefix("-----BEGIN") {
        Some(raw)
      } else {
        match read_file_text(raw) {
          Some(pem) => Some(pem)
          None => {
            missing.push("\{name} (not a PEM and not a readable file: \{raw})")
            None
          }
        }
      }
    None => {
      missing.push(name)
      None
    }
  }
}

///|
/// Read a UTF-8 text file, returning None instead of aborting.
async fn read_file_text(path : String) -> String? {
  let data = try @fs.read_file(path) catch {
    _ => None
  } noraise {
    data => Some(data)
  }
  match data {
    Some(data) => {
      let text : String = try @utf8.decode(data.binary()[:]) catch {
        _ => ""
      } noraise {
        text => text
      }
      if text == "" {
        None
      } else {
        Some(text)
      }
    }
    None => None
  }
}

///|
/// Read a repository asset; the webdemo is useless without them, so abort.
async fn read_asset(path : String) -> String {
  match read_file_text(path) {
    Some(text) => text
    None =>
      abort(
        "cannot read asset \{path} — run the webdemo from the repository root",
      )
  }
}

///|
async fn route(
  app : App,
  index_html : String,
  alipay_html : String,
  qrcode_js : String,
  req : @http.Request,
  reader : &@io.Reader,
  conn : @http.ServerConnection,
) -> Unit {
  let path = req.path
  if req.meth is @http.Post && path == "/api/wechat/notify" {
    let body = reader.read_all().binary()
    handle_notify(app, req, body, conn)
    return
  }
  if req.meth is @http.Post && path == "/api/alipay/notify" {
    let body = reader.read_all().binary()
    handle_alipay_notify(app, body, conn)
    return
  }
  // Drain the body so keep-alive connections stay in sync.
  let _ = reader.read_all()
  if req.meth is @http.Get &&
    (path == "/" || path.has_prefix("/?") || path == "/index.html") {
    respond_text(conn, 200, "text/html; charset=utf-8", index_html)
  } else if req.meth is @http.Get &&
    (path == "/alipay" || path.has_prefix("/alipay?")) {
    respond_text(conn, 200, "text/html; charset=utf-8", alipay_html)
  } else if req.meth is @http.Get && path == "/assets/qrcode.js" {
    respond_text(conn, 200, "text/javascript; charset=utf-8", qrcode_js)
  } else if req.meth is @http.Get && path == "/api/config" {
    handle_config(app, conn)
  } else if req.meth is @http.Post &&
    (path == "/api/orders" || path.has_prefix("/api/orders?")) {
    handle_create_order(app, req, conn)
  } else if req.meth is @http.Post &&
    (
      path == "/api/wechat/jsapi/orders" ||
      path.has_prefix("/api/wechat/jsapi/orders?")
    ) {
    handle_create_jsapi_order(app, req, conn)
  } else if req.meth is @http.Post &&
    (
      path == "/api/wechat/miniapp/orders" ||
      path.has_prefix("/api/wechat/miniapp/orders?")
    ) {
    handle_create_miniapp_order(app, req, conn)
  } else if req.meth is @http.Get && path.has_prefix("/api/orders/") {
    handle_query(app, path, conn)
  } else if req.meth is @http.Post &&
    (path == "/api/alipay/orders" || path.has_prefix("/api/alipay/orders?")) {
    handle_create_alipay_order(app, req, conn)
  } else if req.meth is @http.Get && path.has_prefix("/api/alipay/orders/") {
    handle_alipay_query(app, path, conn)
  } else {
    respond_text(conn, 404, "text/plain; charset=utf-8", "not found\n")
  }
}

///|
/// GET /api/config — which gateway pages and panels are enabled.
async fn handle_config(app : App, conn : @http.ServerConnection) -> Unit {
  respond_json(conn, 200, {
    "wechat": app.wechat is Some(_),
    "miniapp": app.miniapp_config is Some(_),
    "alipay": app.alipay is Some(_),
  })
}

///|
/// POST /api/orders?amount_yuan=0.01 — place a Native (QR) order and return
/// its code_url for the page to render.
async fn handle_create_order(
  app : App,
  req : @http.Request,
  conn : @http.ServerConnection,
) -> Unit {
  let client = match app.wechat {
    Some(client) => client
    None => {
      respond_json(conn, 503, {
        "error": "WeChat Pay is not configured (set WXPAY_* and restart)",
      })
      return
    }
  }
  let yuan = match query_param(req.path, key="amount_yuan") {
    Some(yuan) => yuan
    None => "0.01"
  }
  let fen : Int32 = try @moon-pay-sdk.parse_yuan_fen(yuan) catch {
    _ => 0
  } noraise {
    fen => fen
  }
  if fen <= 0 {
    respond_json(conn, 400, { "error": "bad amount_yuan: \{yuan}" })
    return
  }
  let out_trade_no = next_out_trade_no(app)
  let order : @wechat.NativeOrder? = try
    client.native_order(
      out_trade_no~,
      total=fen,
      description="moon-pay-sdk integration test",
      notify_url=app.notify_url,
    )
  catch {
    e => {
      println("native_order failed: \{e}")
      None
    }
  } noraise {
    order => Some(order)
  }
  match order {
    Some(order) => {
      app.orders.set(out_trade_no, {
        amount_fen: fen,
        notify_seen: false,
        miniapp: false,
      })
      println(
        "order \{out_trade_no} \{yuan} yuan, code_url \{order.code_url()}",
      )
      respond_json(conn, 200, {
        "out_trade_no": out_trade_no,
        "amount_fen": fen,
        "code_url": order.code_url(),
      })
    }
    None =>
      respond_json(conn, 502, {
        "error": "placing the order failed (see server log)",
      })
  }
}

///|
/// POST /api/wechat/jsapi/orders?amount_yuan=0.01&openid=o… — place a JSAPI
/// order for payment inside WeChat and return the signed wx.requestPayment
/// parameter set. The order is polled through /api/orders/
/// like a Native order.
async fn handle_create_jsapi_order(
  app : App,
  req : @http.Request,
  conn : @http.ServerConnection,
) -> Unit {
  let client = match app.wechat {
    Some(client) => client
    None => {
      respond_json(conn, 503, {
        "error": "WeChat Pay is not configured (set WXPAY_* and restart)",
      })
      return
    }
  }
  let yuan = match query_param(req.path, key="amount_yuan") {
    Some(yuan) => yuan
    None => "0.01"
  }
  let fen : Int32 = try @moon-pay-sdk.parse_yuan_fen(yuan) catch {
    _ => 0
  } noraise {
    fen => fen
  }
  if fen <= 0 {
    respond_json(conn, 400, { "error": "bad amount_yuan: \{yuan}" })
    return
  }
  let openid = match query_param(req.path, key="openid") {
    Some(openid) if openid != "" => openid
    _ => {
      respond_json(conn, 400, {
        "error": "openid is required (the payer's openid under this appid)",
      })
      return
    }
  }
  let out_trade_no = next_out_trade_no(app)
  let order : @wechat.JsapiOrder? = try
    client.jsapi_order(
      out_trade_no~,
      total=fen,
      description="moon-pay-sdk integration test",
      notify_url=app.notify_url,
      openid~,
    )
  catch {
    e => {
      println("jsapi_order failed: \{e}")
      None
    }
  } noraise {
    order => Some(order)
  }
  match order {
    Some(order) => {
      let params = client.jsapi_pay_params(prepay_id=order.prepay_id()) catch {
        e => {
          println("jsapi_pay_params failed: \{e}")
          respond_json(conn, 502, {
            "error": "signing the pay params failed (see server log)",
          })
          return
        }
      }
      app.orders.set(out_trade_no, {
        amount_fen: fen,
        notify_seen: false,
        miniapp: false,
      })
      println(
        "jsapi order \{out_trade_no} \{yuan} yuan, prepay_id \{order.prepay_id()}",
      )
      respond_json(conn, 200, {
        "out_trade_no": out_trade_no,
        "amount_fen": fen,
        "prepay_id": order.prepay_id(),
        "pay_params": {
          "appId": params.app_id(),
          "timeStamp": params.timestamp(),
          "nonceStr": params.nonce_str(),
          "package": params.pay_package(),
          "signType": params.sign_type(),
          "paySign": params.pay_sign(),
        },
      })
    }
    None =>
      respond_json(conn, 502, {
        "error": "placing the order failed (see server log)",
      })
  }
}

///|
/// POST /api/wechat/miniapp/orders?amount_yuan=0.01&js_code=… — exchange the
/// wx.login code for the payer's openid, place the order and return the
/// signed wx.requestPayment parameter set, in one call. The order polls
/// through /api/orders/ like a Native order.
async fn handle_create_miniapp_order(
  app : App,
  req : @http.Request,
  conn : @http.ServerConnection,
) -> Unit {
  let config = match app.miniapp_config {
    Some(config) => config
    None => {
      respond_json(conn, 503, {
        "error": "mini-program pay is not configured (set WXPAY_APP_SECRET or WXPAY_MINIAPP_APP_SECRET and restart)",
      })
      return
    }
  }
  // The panel orders under the mini program's appid when it built its own
  // pay client; otherwise it shares the main pay client.
  let pay = match app.miniapp_pay {
    Some(pay) => pay
    None =>
      match app.wechat {
        Some(pay) => pay
        None => {
          respond_json(conn, 503, {
            "error": "WeChat Pay is not configured (set the WXPAY_* variables and restart)",
          })
          return
        }
      }
  }
  let yuan = match query_param(req.path, key="amount_yuan") {
    Some(yuan) => yuan
    None => "0.01"
  }
  let fen : Int32 = try @moon-pay-sdk.parse_yuan_fen(yuan) catch {
    _ => 0
  } noraise {
    fen => fen
  }
  if fen <= 0 {
    respond_json(conn, 400, { "error": "bad amount_yuan: \{yuan}" })
    return
  }
  let js_code = match query_param(req.path, key="js_code") {
    Some(code) if code != "" => code
    _ => {
      respond_json(conn, 400, {
        "error": "js_code is required (from wx.login; single-use, expires in 5 minutes)",
      })
      return
    }
  }
  let out_trade_no = next_out_trade_no(app)
  // Step 1 — exchange the code under the mini-program's own credentials.
  let session : @miniprogram.Session? = try
    @miniprogram.code2session_with(
      config~,
      transport=@transport.HttpClient::new(),
      js_code~,
    )
  catch {
    e => {
      println("miniapp code exchange failed: \{e}")
      None
    }
  } noraise {
    session => Some(session)
  }
  let openid = match session {
    Some(session) => session.openid()
    None => {
      respond_json(conn, 502, {
        "error": "the login code exchange failed (see server log)",
      })
      return
    }
  }
  // Step 2 — place the JSAPI order under the pay client's credentials.
  let outcome : (@wechat.JsapiPayParams, String)? = try {
    let order = pay.jsapi_order(
      out_trade_no~,
      total=fen,
      description="moon-pay-sdk integration test",
      notify_url=app.notify_url,
      openid~,
    )
    let params = pay.jsapi_pay_params(prepay_id=order.prepay_id())
    Some((params, order.prepay_id()))
  } catch {
    e => {
      println("miniapp pay failed: \{e}")
      None
    }
  } noraise {
    outcome => outcome
  }
  match outcome {
    Some((params, prepay_id)) => {
      app.orders.set(out_trade_no, {
        amount_fen: fen,
        notify_seen: false,
        miniapp: true,
      })
      println(
        "miniapp order \{out_trade_no} \{yuan} yuan, openid \{openid}, prepay_id \{prepay_id}",
      )
      respond_json(conn, 200, {
        "out_trade_no": out_trade_no,
        "amount_fen": fen,
        "openid": openid,
        "prepay_id": prepay_id,
        "pay_params": {
          "appId": params.app_id(),
          "timeStamp": params.timestamp(),
          "nonceStr": params.nonce_str(),
          "package": params.pay_package(),
          "signType": params.sign_type(),
          "paySign": params.pay_sign(),
        },
      })
    }
    None =>
      respond_json(conn, 502, {
        "error": "placing the order failed (see server log)",
      })
  }
}

///|
/// GET /api/orders/ — query WeChat Pay for the live trade
/// state so the page can poll without a publicly reachable notify URL.
async fn handle_query(
  app : App,
  path : String,
  conn : @http.ServerConnection,
) -> Unit {
  let out_trade_no = path["/api/orders/".length():].to_owned()
  let rec = match app.orders.get(out_trade_no) {
    Some(rec) => rec
    None => {
      respond_json(conn, 404, { "error": "unknown out_trade_no" })
      return
    }
  }
  // Poll on the merchant that placed the order — the mini-program panel
  // may run under its own appid/mchid pair.
  let owner = if rec.miniapp { app.miniapp_pay } else { app.wechat }
  let client = match owner {
    Some(client) => client
    None => {
      respond_json(conn, 503, {
        "error": "WeChat Pay is not configured (set WXPAY_* and restart)",
      })
      return
    }
  }
  let status : @wechat.OrderStatus? = try
    client.query_order(out_trade_no~)
  catch {
    e => {
      println("query_order failed: \{e}")
      None
    }
  } noraise {
    status => Some(status)
  }
  let status = match status {
    Some(status) => status
    None => {
      respond_json(conn, 502, { "error": "query failed (see server log)" })
      return
    }
  }
  let success_time = match status.success_time() {
    Some(time) => time
    None => ""
  }
  let body : Json = {
    "out_trade_no": status.out_trade_no(),
    "trade_state": trade_state_wire(status.trade_state()),
    "total_fen": status.total_fen(),
    "success_time": success_time,
    "notify_seen": rec.notify_seen,
  }
  respond_json(conn, 200, body)
}

///|
/// POST /api/wechat/notify — WeChat Pay's signed callback. Verify, decrypt,
/// remember, and acknowledge so the gateway stops retrying.
async fn handle_notify(
  app : App,
  req : @http.Request,
  body : Bytes,
  conn : @http.ServerConnection,
) -> Unit {
  let headers : Array[(String, String)] = []
  for key, value in req.headers {
    headers.push((key.to_string(), value))
  }
  // Either merchant may call back: try the main client first, then the
  // mini-program panel's own merchant when it has one.
  let verified = match app.wechat {
    Some(client) => verify_notification(client, headers, body)
    None => None
  }
  let verified = match verified {
    Some(notification) => Some(notification)
    None =>
      match app.miniapp_pay {
        Some(client) => verify_notification(client, headers, body)
        None => None
      }
  }
  match verified {
    Some(notification) => {
      let out_trade_no = notification.out_trade_no()
      match app.orders.get(out_trade_no) {
        Some(rec) => {
          rec.notify_seen = true
          if notification.total_fen() != rec.amount_fen {
            println(
              "notify amount mismatch for \{out_trade_no}: \{notification.total_fen()} fen, expected \{rec.amount_fen}",
            )
          }
        }
        None => println("notify for unknown order \{out_trade_no}")
      }
      println(
        "notify verified: \{out_trade_no} \{notification.event_type()} \{trade_state_wire(notification.trade_state())}",
      )
      let (status, ack) = @wechat.ack_success()
      respond_json_raw(conn, status, ack)
    }
    None => {
      println(
        "notify rejected: no configured merchant's keys verified the signature",
      )
      let (status, ack) = @wechat.ack_failure(message="verification failed")
      respond_json_raw(conn, status, ack)
    }
  }
}

///|
/// Verify and decrypt one callback with the given merchant client.
/// `None` means this client's keys did not sign the callback.
async fn verify_notification(
  client : @wechat.Client,
  headers : Array[(String, String)],
  body : Bytes,
) -> @wechat.Notification? {
  try client.verify_callback(headers, body) catch {
    e => {
      println("notify rejected by appid \{client.config.appid}: \{e}")
      None
    }
  } noraise {
    notification => Some(notification)
  }
}

///|
/// POST /api/alipay/orders?amount_yuan=0.01 — build the signed redirect URL
/// for a page-pay order (alipay.trade.page.pay).
async fn handle_create_alipay_order(
  app : App,
  req : @http.Request,
  conn : @http.ServerConnection,
) -> Unit {
  let client = match app.alipay {
    Some(client) => client
    None => {
      respond_json(conn, 503, {
        "error": "Alipay is not configured (set ALIPAY_* and restart)",
      })
      return
    }
  }
  let yuan = match query_param(req.path, key="amount_yuan") {
    Some(yuan) => yuan
    None => "0.01"
  }
  let fen : Int32 = try @moon-pay-sdk.parse_yuan_fen(yuan) catch {
    _ => 0
  } noraise {
    fen => fen
  }
  if fen <= 0 {
    respond_json(conn, 400, { "error": "bad amount_yuan: \{yuan}" })
    return
  }
  let out_trade_no = next_out_trade_no(app)
  let return_url = "http://\{request_host(req)}/alipay?alipay_order=\{out_trade_no}"
  let pay_url : String? = try
    client.page_pay_url(
      out_trade_no~,
      total_fen=fen,
      subject="moon-pay-sdk integration test",
      notify_url=app.alipay_notify_url,
      return_url~,
    )
  catch {
    e => {
      println("page_pay_url failed: \{e}")
      None
    }
  } noraise {
    url => Some(url)
  }
  match pay_url {
    Some(url) => {
      app.orders.set(out_trade_no, {
        amount_fen: fen,
        notify_seen: false,
        miniapp: false,
      })
      println(
        "alipay order \{out_trade_no} \{yuan} yuan, return_url \{return_url}",
      )
      respond_json(conn, 200, {
        "out_trade_no": out_trade_no,
        "amount_fen": fen,
        "pay_url": url,
      })
    }
    None =>
      respond_json(conn, 502, {
        "error": "building the pay URL failed (see server log)",
      })
  }
}

///|
/// GET /api/alipay/orders/ — query Alipay for the live trade
/// state so the page can poll without a publicly reachable notify URL.
async fn handle_alipay_query(
  app : App,
  path : String,
  conn : @http.ServerConnection,
) -> Unit {
  let client = match app.alipay {
    Some(client) => client
    None => {
      respond_json(conn, 503, {
        "error": "Alipay is not configured (set ALIPAY_* and restart)",
      })
      return
    }
  }
  let out_trade_no = path["/api/alipay/orders/".length():].to_owned()
  let rec = match app.orders.get(out_trade_no) {
    Some(rec) => rec
    None => {
      respond_json(conn, 404, { "error": "unknown out_trade_no" })
      return
    }
  }
  let status : @alipay.OrderStatus? = try
    client.query_order(out_trade_no~)
  catch {
    e => {
      println("alipay query_order failed: \{e}")
      None
    }
  } noraise {
    status => Some(status)
  }
  let status = match status {
    Some(status) => status
    None => {
      respond_json(conn, 502, { "error": "query failed (see server log)" })
      return
    }
  }
  let send_pay_date = match status.send_pay_date() {
    Some(date) => date
    None => ""
  }
  let body : Json = {
    "out_trade_no": status.out_trade_no(),
    "trade_status": trade_status_wire(status.trade_status()),
    "total_fen": status.total_fen(),
    "send_pay_date": send_pay_date,
    "notify_seen": rec.notify_seen,
  }
  respond_json(conn, 200, body)
}

///|
/// POST /api/alipay/notify — Alipay's signed async notification. Verify the
/// RSA2 signature and app_id, remember, and answer "success" so Alipay stops
/// retrying.
async fn handle_alipay_notify(
  app : App,
  body : Bytes,
  conn : @http.ServerConnection,
) -> Unit {
  let client = match app.alipay {
    Some(client) => client
    None => {
      respond_text(conn, 200, "text/plain; charset=utf-8", "failure\n")
      return
    }
  }
  let text = @utf8.decode_lossy(body[:])
  let form = @alipay.parse_form_params(text)
  let notify : @alipay.NotifyResult? = try client.verify_notify(form) catch {
    e => {
      println("alipay notify rejected: \{e}")
      None
    }
  } noraise {
    notify => Some(notify)
  }
  match notify {
    Some(notify) => {
      let out_trade_no = notify.out_trade_no()
      match app.orders.get(out_trade_no) {
        Some(rec) => {
          rec.notify_seen = true
          if !notify.amount_matches(rec.amount_fen) {
            println(
              "alipay notify amount mismatch for \{out_trade_no}: \{notify.total_amount()}, expected \{rec.amount_fen} fen",
            )
          }
        }
        None => println("alipay notify for unknown order \{out_trade_no}")
      }
      println(
        "alipay notify verified: \{out_trade_no} \{trade_status_wire(notify.trade_status())}",
      )
      respond_text(
        conn,
        200,
        "text/plain; charset=utf-8",
        @alipay.ack_success(),
      )
    }
    None => respond_text(conn, 200, "text/plain; charset=utf-8", "failure\n")
  }
}

///|
/// The Host header the browser used, so the Alipay return URL lands back on
/// the same origin; falls back to the local listener.
fn request_host(req : @http.Request) -> String {
  for key, value in req.headers {
    let name = key.to_string()
    if name == "Host" || name == "host" {
      return value
    }
  }
  "127.0.0.1:\{LISTEN_PORT}"
}

///|
fn next_out_trade_no(app : App) -> String {
  app.seq = app.seq + 1
  "IT\{@async.now()}\{pad(app.seq, 4)}"
}

///|
fn pad(n : Int32, width : Int32) -> String {
  let s = n.to_string()
  let mut zeros = ""
  let mut left = width - s.length()
  while left > 0 {
    zeros = zeros + "0"
    left = left - 1
  }
  zeros + s
}

///|
/// Map a TradeState back to the wire string the gateway uses.
fn trade_state_wire(state : @wechat.TradeState) -> String {
  match state {
    @wechat.Success => "SUCCESS"
    @wechat.Refund => "REFUND"
    @wechat.NotPay => "NOTPAY"
    @wechat.Closed => "CLOSED"
    @wechat.Revoked => "REVOKED"
    @wechat.UserPaying => "USERPAYING"
    @wechat.PayError => "PAYERROR"
    @wechat.Unknown(s) => s
  }
}

///|
/// Map a TradeStatus back to the wire string the gateway uses.
fn trade_status_wire(status : @alipay.TradeStatus) -> String {
  match status {
    WaitBuyerPay => "WAIT_BUYER_PAY"
    TradeClosed => "TRADE_CLOSED"
    TradeSuccess => "TRADE_SUCCESS"
    TradeFinished => "TRADE_FINISHED"
    Unknown(s) => s
  }
}

///|
fn query_param(path : String, key~ : String) -> String? {
  let start = match path.find("?") {
    Some(i) => i
    None => return None
  }
  let mut rest = path[start + 1:].to_owned()
  while rest != "" {
    let piece = match rest.find("&") {
      Some(i) => {
        let head = rest[:i].to_owned()
        rest = rest[i + 1:].to_owned()
        head
      }
      None => {
        let head = rest
        rest = ""
        head
      }
    }
    let prefix = "\{key}="
    if piece.has_prefix(prefix) {
      return Some(piece[prefix.length():].to_owned())
    }
  }
  None
}

///|
async fn respond_json(
  conn : @http.ServerConnection,
  status : Int32,
  body : Json,
) -> Unit {
  respond_text(
    conn,
    status,
    "application/json; charset=utf-8",
    @moon-pay-sdk.encode_json(body),
  )
}

///|
async fn respond_json_raw(
  conn : @http.ServerConnection,
  status : Int32,
  body : String,
) -> Unit {
  respond_text(conn, status, "application/json; charset=utf-8", body)
}

///|
async fn respond_text(
  conn : @http.ServerConnection,
  status : Int32,
  content_type : String,
  body : String,
) -> Unit {
  conn
  ..send_response(status, "OK", extra_headers={ "Content-Type": content_type })
  ..write_string(body)
  .end_response()
}