///|
fn build_command_host(
  manifest : @manifest.AppManifest,
  command_extensions : Map[String, @proton_command.AppCommandExtensionSpec],
  base_dir : String?,
) -> Result[@core.AppCommandHost?, String] {
  let host = @core.AppCommandHost::new()
  let mut has_commands = false
  for id, spec in command_extensions {
    match manifest.extension_setting(id) {
      Some(setting) =>
        if setting.is_enabled() {
          let context = @proton_command.AppCommandExtensionContext::new(
            host,
            id,
            spec.js_namespace(),
            options=setting.options(),
            base_dir~,
          )
          match spec.register(context) {
            Err(error) => {
              host.close()
              return Err(error)
            }
            Ok(_) => has_commands = true
          }
        }
      None => ()
    }
  }
  if has_commands {
    host.seal_registrations()
    Ok(Some(host))
  } else {
    host.close()
    Ok(None)
  }
}

///|
fn build_command_proxy_script(
  manifest : @manifest.AppManifest,
  command_extensions : Map[String, @proton_command.AppCommandExtensionSpec],
) -> Result[String?, String] {
  let specs : Array[Json] = []
  let scripts : Array[String] = []
  for id, spec in command_extensions {
    match manifest.extension_setting(id) {
      Some(setting) =>
        if setting.is_enabled() {
          let js_namespace = spec.js_namespace()
          if command_proxy_name_is_invalid(js_namespace) {
            return Err(
              "command extension namespace cannot be exposed to JavaScript: " +
              js_namespace,
            )
          }
          let apis : Array[Json] = []
          for api in spec.apis() {
            let api_name = api.api_name()
            if command_proxy_name_is_invalid(api_name) {
              return Err(
                "command extension API cannot be exposed to JavaScript: " +
                js_namespace +
                "." +
                api_name,
              )
            }
            apis.push(Json::string(api_name))
          }
          specs.push(
            Json::object({
              "namespace": Json::string(js_namespace),
              "apis": Json::array(apis),
            }),
          )
          for script in spec.scripts() {
            scripts.push(script)
          }
        }
      None => ()
    }
  }
  if specs.length() == 0 {
    Ok(None)
  } else {
    let script = command_proxy_bootstrap_script(Json::array(specs).stringify())
    if scripts.length() == 0 {
      Ok(Some(script))
    } else {
      Ok(Some(script + "\n" + scripts.join("\n")))
    }
  }
}

///|
fn command_proxy_name_is_invalid(name : String) -> Bool {
  name.trim().to_owned() == "" ||
  name == "core" ||
  name == "events" ||
  @proton_command.app_command_js_property_is_reserved(name)
}

///|
fn command_proxy_bootstrap_script(specs_json : String) -> String {
  "(function(){\n" +
  "  var specs = " +
  specs_json +
  ";\n" +
  "  var existing = window.__MoonBit__;\n" +
  "  var root = existing && typeof existing === 'object' ? existing : {};\n" +
  "  var exactListeners = Object.create(null);\n" +
  "  var events = root.events && typeof root.events === 'object' ? root.events : {};\n" +
  "  function ensureExactListeners(name) {\n" +
  "    if (!Array.isArray(exactListeners[name])) { exactListeners[name] = []; }\n" +
  "    return exactListeners[name];\n" +
  "  }\n" +
  "  function emitExact(name, event) {\n" +
  "    var listeners = ensureExactListeners(name).slice();\n" +
  "    for (var index = 0; index < listeners.length; index += 1) {\n" +
  "      listeners[index](event);\n" +
  "    }\n" +
  "    return event;\n" +
  "  }\n" +
  "  events.emit = function(name, event) { return emitExact(String(name), event); };\n" +
  "  events['@@emitExtensionEvent'] = function(namespace, eventName, payload) {\n" +
  "    var event = {\n" +
  "      extension: String(namespace),\n" +
  "      name: String(eventName),\n" +
  "      payload: payload === undefined ? null : payload\n" +
  "    };\n" +
  "    return emitExact(event.extension + '.' + event.name, event);\n" +
  "  };\n" +
  "  events.on = function(name, listener) {\n" +
  "    if (typeof listener !== 'function') {\n" +
  "      throw new TypeError('MoonBit.events.on expects a listener function');\n" +
  "    }\n" +
  "    var key = String(name);\n" +
  "    ensureExactListeners(key).push(listener);\n" +
  "    return function() {\n" +
  "      var listeners = ensureExactListeners(key);\n" +
  "      exactListeners[key] = listeners.filter(function(candidate) { return candidate !== listener; });\n" +
  "    };\n" +
  "  };\n" +
  "  root.events = events;\n" +
  "  function invokeExtension(namespace, apiName, payload) {\n" +
  "    var current = window.__MoonBit__;\n" +
  "    var core = current && current.core;\n" +
  "    if (!core || typeof core.invokeOp !== 'function') {\n" +
  "      return Promise.reject(new Error('MoonBit bridge is not available'));\n" +
  "    }\n" +
  "    return core.invokeOp('ext:' + namespace + '/' + apiName, payload);\n" +
  "  }\n" +
  "  var extensions = root['@@extensions'] && typeof root['@@extensions'] === 'object' ? root['@@extensions'] : Object.create(null);\n" +
  "  root['@@extensions'] = extensions;\n" +
  "  root['@@ensureExtension'] = function(extensionName) {\n" +
  "    var existingExtension = extensions[extensionName];\n" +
  "    if (existingExtension && typeof existingExtension === 'object') {\n" +
  "      return existingExtension;\n" +
  "    }\n" +
  "    var target = {};\n" +
  "    target.invoke = function(apiName, payload) {\n" +
  "      return invokeExtension(String(extensionName), String(apiName), payload);\n" +
  "    };\n" +
  "    target.on = function(eventName, listener) {\n" +
  "      return events.on(String(extensionName) + '.' + String(eventName), listener);\n" +
  "    };\n" +
  "    Object.defineProperty(target, 'name', { value: String(extensionName), enumerable: true });\n" +
  "    extensions[extensionName] = target;\n" +
  "    root[extensionName] = target;\n" +
  "    return target;\n" +
  "  };\n" +
  "  for (var i = 0; i < specs.length; i += 1) {\n" +
  "    var namespace = String(specs[i].namespace || '');\n" +
  "    var apis = Array.isArray(specs[i].apis) ? specs[i].apis : [];\n" +
  "    if (!namespace) { continue; }\n" +
  "    var target = root['@@ensureExtension'](namespace);\n" +
  "    target.invoke = (function(ns) {\n" +
  "      return function(apiName, payload) {\n" +
  "        return invokeExtension(ns, String(apiName), payload);\n" +
  "      };\n" +
  "    })(namespace);\n" +
  "    target.on = (function(ns) {\n" +
  "      return function(eventName, listener) {\n" +
  "        return events.on(ns + '.' + String(eventName), listener);\n" +
  "      };\n" +
  "    })(namespace);\n" +
  "    for (var j = 0; j < apis.length; j += 1) {\n" +
  "      var apiName = String(apis[j] || '');\n" +
  "      if (!apiName || apiName === 'then') { continue; }\n" +
  "      target[apiName] = (function(ns, api) {\n" +
  "        return function(payload) { return invokeExtension(ns, api, payload); };\n" +
  "      })(namespace, apiName);\n" +
  "    }\n" +
  "    root[namespace] = target;\n" +
  "  }\n" +
  "  window.__MoonBit__ = root;\n" +
  "})();"
}

///|
fn install_extension_event_sender(
  window : @native.Window,
  host : @core.AppCommandHost,
) -> Unit {
  host.set_event_sender(extension_event_sender(window, None))
}

///|
fn extension_event_sender(
  window : @native.Window,
  page_instance : String?,
) -> async (@ipc.IpcExtensionEvent) -> Unit {
  async fn(event) {
    ignore(window.eval(extension_event_dispatch_script(event, page_instance)))
    @async.sleep(0)
  }
}

///|
fn extension_event_dispatch_script(
  event : @ipc.IpcExtensionEvent,
  page_instance : String?,
) -> String {
  let page_instance_check = match page_instance {
    Some(value) =>
      "  if (!root || !root.core || root.core['@@pageInstance'] !== " +
      Json::string(value).stringify() +
      ") { return; }\n"
    None => ""
  }
  "(function(){\n" +
  "  if (!window.location || window.location.protocol !== 'proton:') { return; }\n" +
  "  var event = " +
  ToJson::to_json(event).stringify() +
  ";\n" +
  "  var root = window.__MoonBit__;\n" +
  page_instance_check +
  "  var events = root && root.events;\n" +
  "  var emit = events && events['@@emitExtensionEvent'];\n" +
  "  if (typeof emit === 'function') {\n" +
  "    emit(event.extension_namespace, event.event_name, event.payload);\n" +
  "  }\n" +
  "})();"
}

///|
async fn dispatch_bridge_request(
  window : @native.Window,
  host : @core.AppCommandHost,
  request : @native.BridgeRequest,
) -> @native.BridgeResponse {
  let request_id = request.request_id()
  let response = host.dispatch_ipc_with_event_sender(
    @ipc.IpcOpRequest::new(
      request_id.to_string(),
      request.op(),
      request.payload(),
    ),
    Some(extension_event_sender(window, request.page_instance())),
  )
  if response.is_ok() {
    @native.BridgeResponse::Ok(request_id~, payload=response.body)
  } else {
    let message = match response.body {
      String(text) => text
      _ => response.body.stringify()
    }
    @native.BridgeResponse::Err(request_id~, code="op_failed", message~)
  }
}