///| Route ownership manifest for Sol/host Worker composition.

///|
pub(all) enum RouteManifestMethod {
  Any
  Get
  Post
  Put
  Delete
  Patch
} derive(Eq, Debug)

///|
pub(all) enum RouteManifestOwner {
  SolPage
  SolApi
  Raw
  StaticAsset
  Reserved
} derive(Eq, Debug)

///|
pub(all) enum RouteManifestMatchKind {
  Exact
  Pattern
  Prefix
} derive(Eq, Debug)

///|
pub struct RouteManifestEntry {
  pattern : String
  http_method : RouteManifestMethod
  owner : RouteManifestOwner
  match_kind : RouteManifestMatchKind
  param_names : Array[String]
}

///|
pub struct RouteOwnershipManifest {
  entries : Array[RouteManifestEntry]
}

///|
fn route_manifest_method_to_string(http_method : RouteManifestMethod) -> String {
  match http_method {
    Any => "ANY"
    Get => "GET"
    Post => "POST"
    Put => "PUT"
    Delete => "DELETE"
    Patch => "PATCH"
  }
}

///|
fn route_manifest_owner_to_string(owner : RouteManifestOwner) -> String {
  match owner {
    SolPage => "sol-page"
    SolApi => "sol-api"
    Raw => "raw"
    StaticAsset => "static-asset"
    Reserved => "reserved"
  }
}

///|
fn route_manifest_match_kind_to_string(
  match_kind : RouteManifestMatchKind,
) -> String {
  match match_kind {
    Exact => "exact"
    Pattern => "pattern"
    Prefix => "prefix"
  }
}

///|
pub fn RouteManifestEntry::to_json(self : RouteManifestEntry) -> Json {
  {
    "pattern": self.pattern.to_json(),
    "method": route_manifest_method_to_string(self.http_method).to_json(),
    "owner": route_manifest_owner_to_string(self.owner).to_json(),
    "match": route_manifest_match_kind_to_string(self.match_kind).to_json(),
    "params": self.param_names.to_json(),
  }
}

///|
pub fn RouteOwnershipManifest::to_json(self : RouteOwnershipManifest) -> Json {
  let entries : Array[Json] = []
  for entry in self.entries {
    entries.push(entry.to_json())
  }
  { "routes": entries.to_json() }
}

///|
fn route_manifest_method_to_js_prefix(
  http_method : RouteManifestMethod,
) -> String {
  match http_method {
    Any => "request"
    Get => "get"
    Post => "post"
    Put => "put"
    Delete => "delete"
    Patch => "patch"
  }
}

///|
fn route_manifest_is_separator(c : Char) -> Bool {
  c == '-' || c == '_' || c == '.' || c == ' '
}

///|
fn route_manifest_upper_ascii(c : Char) -> Char {
  if c >= 'a' && c <= 'z' {
    Char::from_int(c.to_int() - 32)
  } else {
    c
  }
}

///|
fn route_manifest_segment_token(segment : String) -> String {
  let raw = if segment.has_prefix("@") {
    segment[1:].to_owned()
  } else {
    segment
  }
  if raw.has_prefix(":") && raw.length() > 1 {
    raw[1:].to_owned()
  } else if raw.has_prefix("[[...") && raw.has_suffix("]]") {
    raw[5:raw.length() - 2].to_owned()
  } else if raw.has_prefix("[...") && raw.has_suffix("]") {
    raw[4:raw.length() - 1].to_owned()
  } else if raw.has_prefix("[") && raw.has_suffix("]") {
    raw[1:raw.length() - 1].to_owned()
  } else {
    raw
  }
}

///|
fn route_manifest_path_to_pascal(path : String) -> String {
  if path == "/" {
    return "Root"
  }
  let buf = StringBuilder::new()
  let mut capitalize_next = true
  let mut wrote = false
  for segment_view in path.split("/") {
    let segment = route_manifest_segment_token(segment_view.to_owned())
    if segment.is_empty() {
      continue
    }
    capitalize_next = true
    for c in segment {
      let is_alpha = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
      let is_digit = c >= '0' && c <= '9'
      if is_alpha || is_digit {
        if capitalize_next {
          buf.write_char(route_manifest_upper_ascii(c))
        } else {
          buf.write_char(c)
        }
        capitalize_next = false
        wrote = true
      } else if route_manifest_is_separator(c) {
        capitalize_next = true
      }
    }
  }
  if wrote { buf.to_string() } else { "Root" }
}

///|
fn route_manifest_api_helper_name(entry : RouteManifestEntry) -> String {
  route_manifest_method_to_js_prefix(entry.http_method) +
  route_manifest_path_to_pascal(entry.pattern)
}

///|
fn route_manifest_param_type(params : Array[String]) -> String {
  let buf = StringBuilder::new()
  buf.write_string("{ ")
  for i, param in params {
    if i > 0 {
      buf.write_string("; ")
    }
    buf.write_string(route_manifest_ts_string_literal(param))
    buf.write_string(": string")
  }
  buf.write_string(" }")
  buf.to_string()
}

///|
fn route_manifest_ts_string_literal(value : String) -> String {
  "\"" +
  value
  .replace(old="\\", new="\\\\")
  .replace(old="\"", new="\\\"")
  .replace(old="\n", new="\\n")
  .replace(old="\r", new="\\r")
  .replace(old="\t", new="\\t") +
  "\""
}

///|
fn route_manifest_write_api_helper(
  buf : StringBuilder,
  entry : RouteManifestEntry,
) -> Unit {
  let name = route_manifest_api_helper_name(entry)
  let http_method_string = route_manifest_method_to_string(entry.http_method)
  let path = entry.pattern
  let has_params = !entry.param_names.is_empty()
  let path_expr = if has_params {
    "fillSolApiPath(\{route_manifest_ts_string_literal(path)}, params)"
  } else {
    route_manifest_ts_string_literal(path)
  }
  buf.write_string("  \{name}: ")
  if entry.http_method is Get {
    if has_params {
      buf.write_string(
        "(params: \{route_manifest_param_type(entry.param_names)}, init?: RequestInit) =>\n",
      )
    } else {
      buf.write_string("(init?: RequestInit) =>\n")
    }
    buf.write_string(
      "    solApiRequest(\{path_expr}, { ...init, method: \"\{http_method_string}\" }),\n",
    )
  } else {
    if has_params {
      buf.write_string(
        "(params: \{route_manifest_param_type(entry.param_names)}, body: TBody, init?: RequestInit) =>\n",
      )
    } else {
      buf.write_string(
        "(body: TBody, init?: RequestInit) =>\n",
      )
    }
    buf.write_string("    solApiRequest(\{path_expr}, {\n")
    buf.write_string("      ...init,\n")
    buf.write_string("      method: \"\{http_method_string}\",\n")
    buf.write_string("      headers: jsonHeaders(init),\n")
    buf.write_string("      body: JSON.stringify(body),\n")
    buf.write_string("    }),\n")
  }
}

///|
pub fn RouteOwnershipManifest::api_client_manifest_json(
  self : RouteOwnershipManifest,
) -> Json {
  let routes : Array[Json] = []
  for entry in self.entries {
    if entry.owner is SolApi {
      routes.push({
        "name": route_manifest_api_helper_name(entry).to_json(),
        "method": route_manifest_method_to_string(entry.http_method).to_json(),
        "path": entry.pattern.to_json(),
        "params": entry.param_names.to_json(),
      })
    }
  }
  { "apis": routes.to_json() }
}

///|
/// Generate optional TypeScript helpers for Sol JSON API routes.
///
/// The generated helpers carry the stable route path and HTTP method, expose
/// path params where present, and return a typed success/error result shape.
pub fn RouteOwnershipManifest::api_client_typescript(
  self : RouteOwnershipManifest,
) -> String {
  let buf = StringBuilder::new()
  buf.write_string(
    "export type SolApiError = { ok: false; status: number; message: string; response: Response };\n",
  )
  buf.write_string(
    "export type SolApiSuccess = { ok: true; data: T; response: Response };\n",
  )
  buf.write_string(
    "export type SolApiResult = SolApiSuccess | SolApiError;\n\n",
  )
  buf.write_string(
    "function jsonHeaders(init?: RequestInit): Headers {\n  const headers = new Headers(init?.headers);\n  if (!headers.has(\"content-type\")) headers.set(\"content-type\", \"application/json\");\n  return headers;\n}\n\n",
  )
  buf.write_string(
    "function fillSolApiPath(path: string, params: Record): string {\n  return path\n    .replace(/:([A-Za-z0-9_]+)/g, (_, key) => encodeURIComponent(params[key] ?? \"\"))\n    .replace(/\\[\\[?\\.\\.\\.([^\\]]+)\\]?\\]/g, (_, key) => encodeURIComponent(params[key] ?? \"\"))\n    .replace(/\\[([^\\]]+)\\]/g, (_, key) => encodeURIComponent(params[key] ?? \"\"));\n}\n\n",
  )
  buf.write_string(
    "async function solApiRequest(path: string, init: RequestInit): Promise> {\n  const response = await fetch(path, init);\n  const text = await response.text();\n  let data: unknown = null;\n  if (text.length > 0) {\n    try { data = JSON.parse(text); } catch { data = text; }\n  }\n  if (!response.ok) {\n    const message = typeof data === \"object\" && data !== null && \"error\" in data\n      ? String((data as { error?: unknown }).error)\n      : response.statusText;\n    return { ok: false, status: response.status, message, response };\n  }\n  return { ok: true, data: data as T, response };\n}\n\n",
  )
  buf.write_string("export const api = {\n")
  for entry in self.entries {
    if entry.owner is SolApi {
      route_manifest_write_api_helper(buf, entry)
    }
  }
  buf.write_string("} as const;\n")
  buf.to_string()
}

///|
fn push_manifest_entry(
  entries : Array[RouteManifestEntry],
  pattern : String,
  http_method : RouteManifestMethod,
  owner : RouteManifestOwner,
  match_kind : RouteManifestMatchKind,
  param_names : Array[String],
) -> Unit {
  entries.push({ pattern, http_method, owner, match_kind, param_names })
}

///|
fn route_manifest_method_matches(
  entry_method : RouteManifestMethod,
  request_method : RouteManifestMethod,
) -> Bool {
  match entry_method {
    Any => true
    Get => request_method is Get
    Post => request_method is Post
    Put => request_method is Put
    Delete => request_method is Delete
    Patch => request_method is Patch
  }
}

///|
fn route_manifest_method_for_kind(kind : SolRouteKind) -> RouteManifestMethod {
  match kind {
    Page => Get
    GetApi => Get
    PostApi => Post
    PutApi => Put
    DeleteApi => Delete
    PatchApi => Patch
    RawGet => Get
    RawPost => Post
    RawPut => Put
    RawDelete => Delete
    RawPatch => Patch
  }
}

///|
fn route_manifest_owner_for_kind(kind : SolRouteKind) -> RouteManifestOwner {
  match kind {
    Page => SolPage
    GetApi | PostApi | PutApi | DeleteApi | PatchApi => SolApi
    RawGet | RawPost | RawPut | RawDelete | RawPatch => Raw
  }
}

///|
fn route_manifest_match_kind_for_route(
  route : CompiledSolRoute,
) -> RouteManifestMatchKind {
  if !route.param_names.is_empty() || route.catch_all is Some(_) {
    Pattern
  } else {
    Exact
  }
}

///|
fn route_manifest_param_names(route : CompiledSolRoute) -> Array[String] {
  let names = route.param_names.copy()
  match route.catch_all {
    Some(info) => if !names.contains(info.name) { names.push(info.name) }
    None => ()
  }
  names
}

///|
fn is_remote_asset_url(path : String) -> Bool {
  path.has_prefix("http://") ||
  path.has_prefix("https://") ||
  path.has_prefix("//")
}

///|
fn push_asset_manifest_entry(
  entries : Array[RouteManifestEntry],
  path : String,
) -> Unit {
  let url = normalize_asset_url(path)
  if url.is_empty() || is_remote_asset_url(url) {
    return
  }
  for entry in entries {
    if entry.pattern == url &&
      entry.owner is StaticAsset &&
      entry.match_kind is Exact {
      return
    }
  }
  push_manifest_entry(entries, url, Any, StaticAsset, Exact, [])
}

///|
fn add_route_asset_entries(
  entries : Array[RouteManifestEntry],
  route : CompiledSolRoute,
) -> Unit {
  for path in route.styles {
    push_asset_manifest_entry(entries, path)
  }
  for path in route.scripts {
    push_asset_manifest_entry(entries, path)
  }
}

///|
fn add_config_asset_entries(
  entries : Array[RouteManifestEntry],
  config : RouterConfig,
) -> Unit {
  for path in config.styles {
    push_asset_manifest_entry(entries, path)
  }
  for path in config.client_scripts {
    push_asset_manifest_entry(entries, path)
  }
}

///|
fn add_reserved_manifest_entries(entries : Array[RouteManifestEntry]) -> Unit {
  push_manifest_entry(entries, "/_sol/*", Any, Reserved, Prefix, [])
  push_manifest_entry(entries, "/__sol__/*", Any, Reserved, Prefix, [])
  push_manifest_entry(entries, "/_luna/*", Any, Reserved, Prefix, [])
  push_manifest_entry(entries, "/static/*", Any, StaticAsset, Prefix, [])
}

///|
pub fn route_ownership_manifest(
  routes : Array[SolRoutes],
  config? : RouterConfig = RouterConfig::default(),
) -> RouteOwnershipManifest {
  let entries : Array[RouteManifestEntry] = []
  add_reserved_manifest_entries(entries)
  let compiled = compile_sol_routes(routes)
  for route in compiled {
    push_manifest_entry(
      entries,
      route.pattern,
      route_manifest_method_for_kind(route.kind),
      route_manifest_owner_for_kind(route.kind),
      route_manifest_match_kind_for_route(route),
      route_manifest_param_names(route),
    )
    add_route_asset_entries(entries, route)
  }
  add_config_asset_entries(entries, config)
  { entries, }
}

///|
fn route_manifest_path_for_match(path : String) -> String {
  normalize_path(strip_query_string(path))
}

///|
fn route_manifest_prefix_matches(pattern : String, path : String) -> Bool {
  let prefix = if pattern.has_suffix("/*") {
    pattern[:pattern.length() - 2].to_owned()
  } else {
    pattern
  }
  path == prefix || path.has_prefix(prefix + "/")
}

///|
fn is_pattern_param_segment(segment : String) -> Bool {
  if segment.has_prefix(":") && segment.length() > 1 {
    return true
  }
  segment.has_prefix("[") &&
  !segment.has_prefix("[...") &&
  !segment.has_prefix("[[...") &&
  segment.has_suffix("]") &&
  segment.length() > 2
}

///|
fn is_required_catch_all_segment(segment : String) -> Bool {
  segment.has_prefix("[...") && segment.has_suffix("]") && segment.length() > 5
}

///|
fn is_optional_catch_all_segment(segment : String) -> Bool {
  segment.has_prefix("[[...") &&
  segment.has_suffix("]]") &&
  segment.length() > 7
}

///|
fn route_manifest_pattern_matches(pattern : String, path : String) -> Bool {
  let pattern_segments = split_by_char(normalize_path(pattern), '/')
  let path_segments = split_by_char(path, '/')
  let mut pattern_index = 0
  let mut path_index = 0
  while pattern_index < pattern_segments.length() {
    let segment = pattern_segments[pattern_index]
    if is_optional_catch_all_segment(segment) {
      return true
    }
    if is_required_catch_all_segment(segment) {
      return path_index < path_segments.length()
    }
    if path_index >= path_segments.length() {
      return false
    }
    if is_pattern_param_segment(segment) {
      pattern_index += 1
      path_index += 1
      continue
    }
    if segment != path_segments[path_index] {
      return false
    }
    pattern_index += 1
    path_index += 1
  }
  path_index == path_segments.length()
}

///|
fn route_manifest_entry_matches(
  entry : RouteManifestEntry,
  path : String,
  http_method : RouteManifestMethod,
) -> Bool {
  if !route_manifest_method_matches(entry.http_method, http_method) {
    return false
  }
  match entry.match_kind {
    Exact => path == entry.pattern
    Prefix => route_manifest_prefix_matches(entry.pattern, path)
    Pattern => route_manifest_pattern_matches(entry.pattern, path)
  }
}

///|
pub fn RouteOwnershipManifest::match_path(
  self : RouteOwnershipManifest,
  path : String,
  http_method? : RouteManifestMethod = RouteManifestMethod::Get,
) -> RouteManifestEntry? {
  let normalized = route_manifest_path_for_match(path)
  for entry in self.entries {
    if route_manifest_entry_matches(entry, normalized, http_method) {
      return Some(entry)
    }
  }
  None
}