///|
/// The main application type that holds route mappings, middleware, and WebSocket handlers.
///
/// Routing state is split by access pattern:
/// - `static_routes` — `Map[method, Map[path, handler]]` for O(1) static lookup
/// - `dynamic_routes` — radix tree for O(path_length) dynamic lookup
/// - `route_keys` — flat list of `(method, path)` for introspection via `routes()`
pub struct App {
  /// Path prefix prepended to all routes in this app/group.
  priv base_path : String
  /// Flat list of `(method, path)` for introspection via `routes()`.
  priv route_keys : Array[(String, String)]
  /// Ordered middleware list; each entry is `(base_path_scope, middleware)`.
  priv middlewares : Array[(String, Middleware)]
  /// O(1) lookup for fixed paths: `Map[method, Map[path, handler]]`.
  priv static_routes : Map[String, Map[String, HttpHandler]]
  /// O(path_length) lookup for parameterized paths (`:param`, `*`, `**`).
  priv dynamic_routes : @router.RadixRouter[HttpHandler]
  /// O(1) lookup for fixed WebSocket paths.
  priv ws_static_routes : Map[String, @ws.WebSocketHandler]
  /// O(path_length) lookup for parameterized WebSocket paths.
  /// Uses `"WS"` as a synthetic method key for consistent precedence with HTTP routes.
  priv ws_dynamic_routes : @router.RadixRouter[@ws.WebSocketHandler]
  /// Unique ID scoping the native WebSocket hub for this app instance.
  priv ws_runtime_id : String
  /// Catch-all handler for URLs that match no registered route, set via
  /// `set_not_found_handler`. Tried after the scoped list below misses.
  priv mut not_found_handler : HttpHandler?
  /// Path-scoped not-found handlers contributed by `App::group` merges,
  /// stored as `(absolute_prefix, handler)`. At dispatch time the
  /// longest matching prefix wins, so `/api/v1` beats `/api` regardless
  /// of registration order.
  priv scoped_not_found_handlers : Array[(String, HttpHandler)]
}

///|
let app_instance_counter : Ref[Int] = Ref(0)

///|
fn next_app_instance_id() -> String {
  app_instance_counter.val += 1
  "app-\{app_instance_counter.val}"
}

///|
/// Creates a new App application instance with an optional base path prefix.
pub fn App::App(base_path? : String = "") -> App {
  {
    base_path,
    route_keys: [],
    middlewares: [],
    static_routes: {},
    dynamic_routes: RadixRouter(),
    ws_static_routes: {},
    ws_dynamic_routes: RadixRouter(),
    ws_runtime_id: next_app_instance_id(),
    not_found_handler: None,
    scoped_not_found_handlers: [],
  }
}

///|
/// Returns an iterator over the registered route keys as `(method, path)` pairs.
pub fn App::routes(self : App) -> Iter[(String, String)] {
  self.route_keys.iter()
}

///|
/// Registers a route handler for the given HTTP method and path.
pub fn App::on(
  self : App,
  http_method : String,
  path : String,
  handler : HttpHandler,
) -> Unit {
  let path = self.base_path + path
  self.route_keys.push((http_method, path))

  // Cache routes by path type for faster lookup
  if !path.contains(":") && !path.contains("*") {
    // Static path: cache directly
    if self.static_routes.get(http_method) is Some(method_routes) {
      method_routes.set(path, handler)
    } else {
      let new_routes : Map[String, HttpHandler] = Map::from_array([
        (path, handler),
      ])
      self.static_routes.set(http_method, new_routes)
    }
  } else {
    // Dynamic path: compile and insert into radix tree
    let compiled = @router.CompiledRoute(path)
    self.dynamic_routes.insert(http_method, compiled, handler)
  }
}

///|
/// Registers a noraise GET handler. For most use cases, prefer `get()`.
pub fn App::get_raw(self : App, path : String, handler : HttpHandler) -> Unit {
  self.on("GET", path, handler)
}

///|
/// Registers a noraise POST handler.
pub fn App::post_raw(self : App, path : String, handler : HttpHandler) -> Unit {
  self.on("POST", path, handler)
}

///|
/// Registers a noraise PATCH handler.
pub fn App::patch_raw(self : App, path : String, handler : HttpHandler) -> Unit {
  self.on("PATCH", path, handler)
}

///|
/// Registers a noraise CONNECT handler.
pub fn App::connect_raw(
  self : App,
  path : String,
  handler : HttpHandler,
) -> Unit {
  self.on("CONNECT", path, handler)
}

///|
/// Registers a noraise PUT handler.
pub fn App::put_raw(self : App, path : String, handler : HttpHandler) -> Unit {
  self.on("PUT", path, handler)
}

///|
/// Registers a noraise DELETE handler.
pub fn App::delete_raw(
  self : App,
  path : String,
  handler : HttpHandler,
) -> Unit {
  self.on("DELETE", path, handler)
}

///|
/// Registers a noraise HEAD handler.
pub fn App::head_raw(self : App, path : String, handler : HttpHandler) -> Unit {
  self.on("HEAD", path, handler)
}

///|
/// Registers a noraise OPTIONS handler.
pub fn App::options_raw(
  self : App,
  path : String,
  handler : HttpHandler,
) -> Unit {
  self.on("OPTIONS", path, handler)
}

///|
/// Registers a noraise TRACE handler.
pub fn App::trace_raw(self : App, path : String, handler : HttpHandler) -> Unit {
  self.on("TRACE", path, handler)
}

///|
/// Registers a noraise handler that matches all HTTP methods.
pub fn App::all_raw(self : App, path : String, handler : HttpHandler) -> Unit {
  self.on("*", path, handler)
}

///|
/// Creates a route group with a shared base path prefix, merging its routes and middleware into the app.
pub fn App::group(
  self : App,
  base_path : String,
  configure : (App) -> Unit,
) -> Unit {
  let group = App(base_path=self.base_path + base_path)
  configure(group)
  // Merge route keys for introspection
  self.route_keys.append(group.route_keys)
  // Merge static route tables
  for i in group.static_routes {
    let (http_method, group_routes) = i
    match self.static_routes.get(http_method) {
      Some(existing_routes) =>
        group_routes.iter().each(route => existing_routes.set(route.0, route.1))
      None => self.static_routes.set(http_method, group_routes)
    }
  }
  // Merge dynamic routes (radix tree)
  self.dynamic_routes.merge(group.dynamic_routes)
  group.ws_static_routes.iter().each(i => self.ws_static_routes.set(i.0, i.1))
  self.ws_dynamic_routes.merge(group.ws_dynamic_routes)
  // Merge middlewares
  self.middlewares.append(group.middlewares)
  // Merge scoped not_found handlers from the group, plus the group's own
  // not_found_handler if set. Each entry stores the absolute prefix so
  // the dispatcher can pick the longest match independent of registration
  // order: with /api and /api/v1 both registered, /api/v1/missing fires
  // the /api/v1 handler regardless of which group was merged first.
  for entry in group.scoped_not_found_handlers {
    self.scoped_not_found_handlers.push(entry)
  }
  if group.not_found_handler is Some(group_handler) {
    let group_prefix = normalize_middleware_base_path(group.base_path)
    self.scoped_not_found_handlers.push((group_prefix, group_handler))
  }
}

///|
/// Resolves the not-found handler for a request path: longest-matching
/// scoped handler wins, then the catch-all from `set_not_found_handler`,
/// then the framework default. Used by both live serving and synthetic
/// dispatch so the two stay aligned.
fn App::resolve_not_found_handler(self : App, path : String) -> HttpHandler {
  let mut best : (Int, HttpHandler)? = None
  for entry in self.scoped_not_found_handlers {
    let (prefix, handler) = entry
    if @httputil.path_scope_matches(prefix, path) {
      let score = prefix.length()
      // Strict > so two groups at the same prefix follow later-wins
      // semantics (same as overwriting a key in a Map): the most recently
      // registered handler at that prefix takes precedence.
      match best {
        Some((best_score, _)) if best_score > score => ()
        _ => best = Some((score, handler))
      }
    }
  }
  match best {
    Some((_, handler)) => handler
    None =>
      match self.not_found_handler {
        Some(handler) => handler
        None => handle_not_found()
      }
  }
}

///|
/// Sets a custom handler for 404 Not Found responses.
pub fn App::set_not_found_handler(self : App, handler : HttpHandler) -> Unit {
  self.not_found_handler = Some(handler)
}

///|
/// Returns true if any custom not-found handler has been registered,
/// either as the catch-all via `set_not_found_handler` or via a group.
pub fn App::has_not_found_handler(self : App) -> Bool {
  self.not_found_handler is Some(_) ||
  !self.scoped_not_found_handlers.is_empty()
}

///|
/// Returns all WebSocket runtime IDs scoped to this App instance, in
/// the form `/serve-` (one per active `serve_on`/`serve`
/// invocation). Used in tests to verify runtime registration and
/// cleanup; production code should not need this.
pub fn App::ws_runtime_ids(self : App) -> Array[String] {
  let prefix = self.ws_runtime_id + "/"
  let result : Array[String] = []
  for runtime_id in @ws.registered_runtime_ids() {
    if runtime_id.has_prefix(prefix) {
      result.push(runtime_id)
    }
  }
  result
}

///|
/// Registers a WebSocket handler for the given path, matched regardless of HTTP method.
///
/// Dynamic patterns (`:param`, `*`, `**`) are inserted into the same radix
/// tree implementation used for HTTP routes, giving them consistent precedence:
/// static > param > wildcard > globstar. Re-registering the same path overrides
/// the previous handler.
pub fn App::ws(
  self : App,
  path : String,
  handler : @ws.WebSocketHandler,
) -> Unit {
  let path = self.base_path + path
  // Static path: direct map lookup
  if !path.contains(":") && !path.contains("*") {
    self.ws_static_routes.set(path, handler)
  } else {
    // Dynamic path: insert into radix tree (WS is a synthetic method key)
    let compiled = @router.CompiledRoute(path)
    self.ws_dynamic_routes.insert("WS", compiled, handler)
  }
}