///|
/// Matching mode for a route condition.
pub(all) enum MatchMode {
  Any
  Exact(String)
} derive(Eq, Debug)

///|
/// A declarative WebHook route.
pub(all) struct HookRoute {
  name : String
  provider : MatchMode
  event_type : MatchMode
  enabled : Bool
} derive(Eq, Debug)

///|
/// A router is intentionally declarative so tests and CLIs can inspect it.
pub(all) struct HookRouter {
  routes : Array[HookRoute]
  fallback : HookResult
} derive(Eq, Debug)

///|
/// Build an exact matcher.
pub fn exact(value : StringView) -> MatchMode {
  Exact(value.to_owned())
}

///|
/// Build a route for all providers and all event types.
pub fn HookRoute::HookRoute(
  name : StringView,
  provider? : MatchMode = Any,
  event_type? : MatchMode = Any,
  enabled? : Bool = true,
) -> HookRoute {
  { name: name.to_owned(), provider, event_type, enabled }
}

///|
/// Create an empty router.
pub fn new_hook_router() -> HookRouter {
  { routes: [], fallback: ignored(message="no route matched") }
}

///|
/// Replace the router fallback result.
pub fn HookRouter::with_fallback(
  self : HookRouter,
  fallback : HookResult,
) -> HookRouter {
  { routes: self.routes, fallback }
}

///|
/// Register a route and return the updated router.
pub fn HookRouter::add(self : HookRouter, route : HookRoute) -> HookRouter {
  let routes = self.routes
  routes.push(route)
  { routes, fallback: self.fallback }
}

///|
/// Register an exact provider/event route.
pub fn HookRouter::on(
  self : HookRouter,
  provider : HookProvider,
  event_type : StringView,
  name? : StringView = event_type,
) -> HookRouter {
  self.add(
    HookRoute(
      name,
      provider=exact(provider.slug()),
      event_type=exact(event_type),
    ),
  )
}

///|
/// Register a provider-only route.
pub fn HookRouter::on_provider(
  self : HookRouter,
  provider : HookProvider,
  name? : StringView = "provider-route",
) -> HookRouter {
  self.add(HookRoute(name, provider=exact(provider.slug())))
}

///|
/// Register an event-type-only route.
pub fn HookRouter::on_event(
  self : HookRouter,
  event_type : StringView,
  name? : StringView = event_type,
) -> HookRouter {
  self.add(HookRoute(name, event_type=exact(event_type)))
}

///|
/// Resolve the first matching route.
pub fn HookRouter::match_event(
  self : HookRouter,
  event : HookEvent,
) -> HookRoute? {
  for route in self.routes {
    if route.matches(event) {
      return Some(route)
    }
  }
  None
}

///|
/// Dispatch returns an accepted result for a matched route and fallback otherwise.
pub fn HookRouter::dispatch(self : HookRouter, event : HookEvent) -> HookResult {
  match self.match_event(event) {
    Some(route) => accepted(message="matched route " + route.name)
    None => self.fallback
  }
}

///|
/// Check whether a route accepts an event.
pub fn HookRoute::matches(self : HookRoute, event : HookEvent) -> Bool {
  self.enabled &&
  match_provider(self.provider, event.provider) &&
  match_string(self.event_type, event.event_type)
}

///|
fn match_provider(mode : MatchMode, provider : HookProvider) -> Bool {
  match mode {
    Any => true
    Exact(value) => value == provider.slug()
  }
}

///|
fn match_string(mode : MatchMode, value : String) -> Bool {
  match mode {
    Any => true
    Exact(expected) => expected == value
  }
}

///|
/// Count enabled routes for diagnostics.
pub fn HookRouter::enabled_count(self : HookRouter) -> Int {
  let mut count = 0
  for route in self.routes {
    if route.enabled {
      count = count + 1
    }
  }
  count
}