///|
/// Creates an inline HTML application.
pub fn html(
  title : String,
  html : String,
  width? : Int = 900,
  height? : Int = 700,
  debug? : Bool = false,
  resizable? : Bool = true,
) -> App {
  App::new(
    title,
    width,
    height,
    @manifest.AppEntry::Html(html),
    debug_level_from_bool(debug),
    size_hint_from_resizable(resizable),
  )
}

///|
/// Creates an inline URL application.
pub fn url(
  title : String,
  url : String,
  width? : Int = 900,
  height? : Int = 700,
  debug? : Bool = false,
  resizable? : Bool = true,
) -> App {
  App::new(
    title,
    width,
    height,
    @manifest.AppEntry::Url(url),
    debug_level_from_bool(debug),
    size_hint_from_resizable(resizable),
  )
}

///|
/// Creates an inline file application.
pub fn file(
  title : String,
  path : String,
  width? : Int = 900,
  height? : Int = 700,
  debug? : Bool = false,
  resizable? : Bool = true,
) -> App {
  App::new(
    title,
    width,
    height,
    @manifest.AppEntry::File(path),
    debug_level_from_bool(debug),
    size_hint_from_resizable(resizable),
  )
}

///|
/// Creates an inline asset application.
pub fn asset(
  title : String,
  path : String,
  width? : Int = 900,
  height? : Int = 700,
  debug? : Bool = false,
  resizable? : Bool = true,
) -> App {
  App::new(
    title,
    width,
    height,
    @manifest.AppEntry::Asset(path),
    debug_level_from_bool(debug),
    size_hint_from_resizable(resizable),
  )
}

///|
/// Creates a web contents view configuration for `WindowHandle::add_view`,
/// following the Electron `new WebContentsView(options)` model: the view
/// renders `url` inside its owning window's content area at explicit
/// top-left bounds, stacked above the window's main page.
pub fn view(
  url : String,
  width~ : Int,
  height~ : Int,
  x? : Int = 0,
  y? : Int = 0,
  visible? : Bool = true,
  z_order? : Int = 0,
  background_color? : String,
) -> @native.ViewConfig {
  @native.ViewConfig::new(
    width~,
    height~,
    x~,
    y~,
    visible~,
    z_order~,
    initial_url=url,
    background_color?,
  )
}

///|
/// Creates an application from `PROTON_CONFIG_PATH`, `proton.project.json` in the
/// current working directory, or code-only defaults.
pub fn app() -> App {
  config_app(None, auto_config=true)
}

///|
/// Creates an application backed by a `proton.project.json` config file.
///
/// The default `proton.project.json` path honors `PROTON_CONFIG_PATH` and packaged
/// config discovery. Other paths are used exactly as provided.
pub fn config(path : String) -> App {
  let path = if path == default_config_file {
    implicit_config_path().unwrap_or(path)
  } else {
    path
  }
  config_app(Some(path), auto_config=false)
}

///|
fn config_app(path : String?, auto_config~ : Bool) -> App {
  App::{
    file_path: path,
    auto_config,
    window: @manifest.WindowManifest::new(
      "Proton",
      900,
      700,
      size_hint=@manifest.WindowSizeHint::None,
    ),
    entry: @manifest.AppEntry::Html(""),
    windows: [],
    debug_level_value: 0,
    headless_value: false,
    single_instance_identifier: None,
    title_overridden: false,
    size_overridden: false,
    titlebar_style_overridden: false,
    entry_overridden: false,
    debug_overridden: false,
    bridge_startup_timeout_value_ms: default_bridge_startup_timeout_ms,
    menu: None,
    extension_settings: Map([]),
    permission_grants: [],
    command_extensions: Map([]),
    command_registrars: [],
    application_lifecycle_hooks: [],
    planned_views: [],
    launch_input_handlers: [],
    window_event_handlers: [],
    view_event_handlers: [],
    window_close_handler: None,
    navigation_handler: None,
    popup_handler: None,
    download_handler: None,
    certificate_handler: None,
    media_handler: None,
    download_event_handlers: [],
    update_handlers: [],
    window_lifecycle_hooks: [],
    validation_errors: [],
  }
}

///|
/// Enables or disables runtime debug mode.
pub fn App::debug(self : App, enabled? : Bool = true) -> App {
  self.debug_level(debug_level_from_bool(enabled))
}

///|
/// Sets the runtime debug level.
pub fn App::debug_level(self : App, debug : Int) -> App {
  self.debug_overridden = true
  self.debug_level_value = debug
  self
}

///|
/// Enables or disables off-screen headless rendering for the application.
///
/// Headless mode does not create a native top-level window. Set
/// `PROTON_HEADLESS=1` to force this mode for automated test runs.
pub fn App::headless(self : App, enabled? : Bool = true) -> App {
  self.headless_value = enabled
  self
}

///|
/// Ensures only one operating-system process owns this application identity.
/// Later processes forward their URL, document, or reopen activation and exit.
pub fn App::single_instance(self : App, identifier : String) -> App {
  let identifier = identifier.trim().to_owned()
  if identifier == "" {
    self.validation_errors.push(
      InvalidSetting(
        name="single_instance",
        message="identifier must not be empty",
      ),
    )
  } else {
    self.single_instance_identifier = Some(identifier)
  }
  self
}

///|
/// Sets the maximum time allowed for the native bridge to become ready.
pub fn App::bridge_startup_timeout_ms(self : App, timeout_ms : Int) -> App {
  self.bridge_startup_timeout_value_ms = timeout_ms
  self
}

///|
/// Sets the primary window title.
pub fn App::title(self : App, title : String) -> App {
  self.title_overridden = true
  self.window = @manifest.WindowManifest::new(
    title,
    self.window.width,
    self.window.height,
    size_hint=self.window.size_hint,
    titlebar_style=self.window.titlebar_style,
  )
  self
}

///|
/// Sets the primary window size.
pub fn App::size(self : App, width~ : Int, height~ : Int) -> App {
  self.size_overridden = true
  self.window = @manifest.WindowManifest::new(
    self.window.title,
    width,
    height,
    size_hint=self.window.size_hint,
    titlebar_style=self.window.titlebar_style,
  )
  self
}

///|
/// Sets whether web content remains below or extends beneath the native
/// titlebar. Overlay rendering is currently implemented on macOS and Windows.
pub fn App::titlebar_style(self : App, style : TitlebarStyle) -> App {
  self.titlebar_style_overridden = true
  self.window = @manifest.WindowManifest::new(
    self.window.title,
    self.window.width,
    self.window.height,
    size_hint=self.window.size_hint,
    titlebar_style=match style {
      Default => @manifest.TitlebarStyle::Default
      Overlay => @manifest.TitlebarStyle::Overlay
    },
  )
  self
}

///|
/// Overrides the primary app entry with inline HTML.
pub fn App::entry_html(self : App, html : String) -> App {
  self.set_entry(@manifest.AppEntry::Html(html))
}

///|
/// Overrides the primary app entry with a URL.
pub fn App::entry_url(self : App, url : String) -> App {
  self.set_entry(@manifest.AppEntry::Url(url))
}

///|
/// Overrides the primary app entry with a file path.
pub fn App::entry_file(self : App, path : String) -> App {
  self.set_entry(@manifest.AppEntry::File(path))
}

///|
/// Overrides the primary app entry with an asset path.
pub fn App::entry_asset(self : App, path : String) -> App {
  self.set_entry(@manifest.AppEntry::Asset(path))
}

///|
/// Sets the app-level native menu bar.
pub fn App::menu(self : App, menu : @native.MenuBar) -> App {
  self.menu = Some(menu)
  self
}

///|
/// Registers an application-level handler for URL, file, and reopen inputs.
/// Registers a handler for an update the channel offers.
///
/// Registering one is what turns the automatic check on: an application that
/// has not said what to do when an update exists is not asked to contact a
/// server on launch. The handler runs after the application is up, on the
/// application task group, and never on the startup path.
///
/// It is called with an update that has already been authenticated and found
/// newer. Nothing has been downloaded and nothing will be until the handler
/// asks for it.
pub fn App::on_update_available(
  self : App,
  handler : async (PendingUpdate) -> Unit noraise,
) -> App {
  self.update_handlers.push(handler)
  self
}

///|
pub fn App::on_launch_input(
  self : App,
  handler : async (RuntimeLaunchInput) -> Unit noraise,
) -> App {
  self.launch_input_handlers.push(handler)
  self
}

///|
/// Observes coalesced native state changes for every running window.
pub fn App::on_window_event(
  self : App,
  handler : async (WindowHandle, WindowEvent) -> Unit noraise,
) -> App {
  self.window_event_handlers.push(handler)
  self
}

///|
/// Attaches a web contents view to the primary window at startup, the
/// declarative counterpart of `WindowHandle::add_view`: the view is created
/// with the window and can later be found through `WindowHandle::view(id)`
/// or observed through `App::on_view_event`. Use the imperative
/// `WindowHandle::add_view` for views whose lifetime is dynamic.
pub fn App::with_view(
  self : App,
  id : String,
  config : @native.ViewConfig,
) -> App {
  if id.trim().is_empty() {
    self.validation_errors.push(
      InvalidSetting(name="views.id", message="must not be empty"),
    )
    return self
  }
  if self.planned_views.any(view => view.0 == id) {
    self.validation_errors.push(
      InvalidSetting(name="views.id", message="duplicate view id: " + id),
    )
    return self
  }
  self.planned_views.push((id, config))
  self
}

///|
/// Observes lifecycle events for every running web contents view: loading
/// changes, main-frame navigations, title updates, and load failures.
pub fn App::on_view_event(
  self : App,
  handler : async (ViewHandle, ViewEvent) -> Unit noraise,
) -> App {
  self.view_event_handlers.push(handler)
  self
}

///|
/// Intercepts user-initiated close requests without blocking the native UI
/// thread. `WindowHandle::close` uses the same request path; forced cleanup is
/// reserved for the session-owned destroy lifecycle.
pub fn App::on_window_close_request(
  self : App,
  handler : async (WindowHandle) -> WindowCloseDecision noraise,
) -> App {
  self.window_close_handler = Some(handler)
  self
}

///|
/// Reviews top-level navigations asynchronously. The native browser cancels a
/// pending navigation until this handler returns, then replays it exactly once
/// when allowed.
pub fn App::on_navigation_request(
  self : App,
  handler : async (BrowserHandle, NavigationRequest) -> NavigationDecision noraise,
) -> App {
  self.navigation_handler = Some(handler)
  self
}

///|
/// Reviews `window.open` and new-tab requests. New Proton windows must already
/// be declared with `add_window(..., open_on_start=false)`.
pub fn App::on_popup_request(
  self : App,
  handler : async (BrowserHandle, PopupRequest) -> PopupDecision noraise,
) -> App {
  self.popup_handler = Some(handler)
  self
}

///|
/// Reviews downloads before CEF chooses a destination.
pub fn App::on_download_request(
  self : App,
  handler : async (BrowserHandle, DownloadRequest) -> DownloadDecision noraise,
) -> App {
  self.download_handler = Some(handler)
  self
}

///|
/// Reviews invalid TLS certificates. The default is denial.
pub fn App::on_certificate_error(
  self : App,
  handler : async (BrowserHandle, CertificateError) -> BrowserPermissionDecision noraise,
) -> App {
  self.certificate_handler = Some(handler)
  self
}

///|
/// Reviews camera, microphone, and display-capture requests. The default is
/// denial.
pub fn App::on_media_permission_request(
  self : App,
  handler : async (BrowserHandle, MediaPermissionRequest) -> BrowserPermissionDecision noraise,
) -> App {
  self.media_handler = Some(handler)
  self
}

///|
/// Observes download progress and terminal states.
pub fn App::on_download_event(
  self : App,
  handler : async (BrowserHandle, DownloadEvent) -> Unit noraise,
) -> App {
  self.download_event_handlers.push(handler)
  self
}

///|
/// Adds a secondary window owned by the standard application lifecycle.
pub fn App::add_window(
  self : App,
  id : String,
  title : String,
  entry : AppEntry,
  width? : Int = 900,
  height? : Int = 700,
  size_hint? : WindowSizeHint = WindowSizeHint::Unconstrained,
  titlebar_style? : TitlebarStyle = TitlebarStyle::Default,
  open_on_start? : Bool = true,
) -> App {
  let normalized_id = id.trim().to_owned()
  if normalized_id == "" || normalized_id == "main" {
    self.validation_errors.push(
      InvalidSetting(
        name="window.id",
        message=if normalized_id == "main" {
          "\"main\" is reserved for the primary window"
        } else {
          "must not be empty"
        },
      ),
    )
    return self
  }
  if self.windows.any(fn(window) { window.id == normalized_id }) {
    self.validation_errors.push(
      InvalidSetting(
        name="window.id",
        message="duplicate secondary window id: " + normalized_id,
      ),
    )
    return self
  }
  self.windows.push(
    @manifest.AppWindowManifest::new(
      normalized_id,
      @manifest.WindowManifest::new(
        title,
        width,
        height,
        size_hint=manifest_window_size_hint(size_hint),
        titlebar_style=manifest_titlebar_style(titlebar_style),
      ),
      entry,
      open_on_start~,
    ),
  )
  self
}

///|
fn manifest_window_size_hint(hint : WindowSizeHint) -> @manifest.WindowSizeHint {
  match hint {
    WindowSizeHint::Unconstrained => @manifest.WindowSizeHint::None
    WindowSizeHint::Fixed => @manifest.WindowSizeHint::Fixed
    WindowSizeHint::Min => @manifest.WindowSizeHint::Min
    WindowSizeHint::Max => @manifest.WindowSizeHint::Max
  }
}

///|
fn manifest_titlebar_style(style : TitlebarStyle) -> @manifest.TitlebarStyle {
  match style {
    TitlebarStyle::Default => @manifest.TitlebarStyle::Default
    TitlebarStyle::Overlay => @manifest.TitlebarStyle::Overlay
  }
}

///|
/// Adds one package registrar for typed application commands.
///
/// Registration runs before any window is created and is sealed before the
/// renderer bridge starts accepting requests.
pub fn App::commands(
  self : App,
  register : (@proton_command.CommandRegistrar) -> Unit raise,
) -> App {
  self.command_registrars.push(register)
  self
}

///|
/// Adds a paired application lifecycle hook.
///
/// The startup state is passed to shutdown. Completed hooks shut down in
/// reverse order, including when a later startup hook fails.
pub fn[State] App::app_lifecycle(
  self : App,
  on_start~ : async (ApplicationContext) -> State,
  on_shutdown~ : async (State) -> Unit,
) -> App {
  self.application_lifecycle_hooks.push(ApplicationLifecycleHook::{
    start: async fn(context) {
      let state = on_start(context)
      ApplicationLifecycleActivation::{
        shutdown: async fn() { on_shutdown(state) },
      }
    },
  })
  self
}

///|
/// Adds a paired primary-window lifecycle hook.
///
/// The ready state is passed to close. Completed hooks close in reverse order,
/// including when a later ready hook fails.
pub fn[State] App::window_lifecycle(
  self : App,
  on_ready~ : async (WindowContext) -> State,
  on_close~ : async (State) -> Unit,
) -> App {
  self.window_lifecycle_hooks.push(WindowLifecycleHook::{
    start: async fn(context) {
      let state = on_ready(context)
      WindowLifecycleActivation::{ close: async fn() { on_close(state) } }
    },
  })
  self
}

///|
/// Registers one extension setting with the app facade.
///
/// The native DLL route exposes command extensions through
/// `window.__MoonBit__.core.invokeOp(...)` and generated high-level proxies.
/// The renderer installs the bridge before the page's first script executes.
pub fn App::extension(
  self : App,
  extension : @proton_extension.Extension,
) -> App {
  let id = extension.id()
  if id.trim().to_owned() == "" {
    self.validation_errors.push(
      InvalidSetting(name="extension.id", message="must not be empty"),
    )
  } else {
    self.extension_settings[id] = @manifest.ExtensionSetting::enabled()
    let command_spec = Some(extension.command_spec()) catch {
      error => {
        self.validation_errors.push(
          ExtensionAdaptationFailed(extension_id=id, error~),
        )
        None
      }
    }
    match command_spec {
      Some(spec) => self.command_extensions[id] = spec
      None => ()
    }
  }
  self
}

///|
/// Grants one registered extension to a trusted source in one window.
///
/// Extension registration alone never exposes renderer capabilities.
pub fn App::permission(self : App, grant : @manifest.PermissionGrant) -> App {
  self.permission_grants.push(grant)
  self
}

///|
/// Registers an extension and explicitly exposes it to one trusted page.
///
/// Use `extension` plus `permission` separately when an extension provides a
/// typed permission builder, such as the filesystem extension.
pub fn App::expose(
  self : App,
  extension : @proton_extension.Extension,
  window? : String = "main",
  origin? : @manifest.PermissionOrigin = Entry,
  scope? : Json = Json::empty_object(),
) -> App {
  let extension_id = extension.id()
  ignore(self.extension(extension))
  self.permission(
    @manifest.PermissionGrant::new(window, origin, extension_id, scope~),
  )
}

///|
/// Registers a set of extension settings with the app facade.
pub fn App::extensions(
  self : App,
  extensions : @proton_extension.Extensions,
) -> App {
  for extension in extensions.items() {
    ignore(self.extension(extension))
  }
  self
}