///|
/// Content-Type for a static file path, by extension. Unknown extensions
/// default to application/octet-stream.
pub fn content_type_for(path : String) -> String {
  let lower = path.to_lower()
  if lower.has_suffix(".html") || lower.has_suffix(".htm") {
    "text/html; charset=utf-8"
  } else if lower.has_suffix(".js") || lower.has_suffix(".mjs") {
    "text/javascript; charset=utf-8"
  } else if lower.has_suffix(".css") {
    "text/css; charset=utf-8"
  } else if lower.has_suffix(".json") || lower.has_suffix(".map") {
    "application/json"
  } else if lower.has_suffix(".svg") {
    "image/svg+xml"
  } else if lower.has_suffix(".png") {
    "image/png"
  } else if lower.has_suffix(".jpg") || lower.has_suffix(".jpeg") {
    "image/jpeg"
  } else if lower.has_suffix(".gif") {
    "image/gif"
  } else if lower.has_suffix(".ico") {
    "image/x-icon"
  } else if lower.has_suffix(".txt") {
    "text/plain; charset=utf-8"
  } else if lower.has_suffix(".woff") {
    "font/woff"
  } else if lower.has_suffix(".woff2") {
    "font/woff2"
  } else {
    "application/octet-stream"
  }
}

///|
/// Reject path-traversal attempts before touching the filesystem.
fn static_path_safe(url_path : String) -> Bool {
  let mut previous = ""
  for segment in url_path.split("/") {
    let part = segment.to_owned()
    if part == ".." {
      return false
    }
    ignore(part)
    ignore(previous)
    previous = part
  }
  true
}

///|
/// Resolve a GET URL path to a file inside `dir`: `/` maps to
/// `/index.html`; directories resolve to their `index.html`. Returns None
/// when nothing matches or the path tries to escape `dir`.
pub fn resolve_static(dir : String, url_path : String) -> String? raise {
  if !static_path_safe(url_path) {
    return None
  }
  let relative = if url_path == "/" { "/index.html" } else { url_path }
  let full = if dir.has_suffix("/") {
    dir + relative[1:].to_owned()
  } else {
    dir + relative
  }
  if @fs.path_exists(full) && @fs.is_file(full) {
    Some(full)
  } else if @fs.path_exists(full + "/index.html") {
    Some(full + "/index.html")
  } else {
    None
  }
}

///|
/// Serve a GET request from `dir`. Returns None when no file matches so
/// the caller can fall back (404 or dynamic dispatch). Read errors degrade
/// to None. Text content types are served through the string body; binary
/// types through the bytes body.
pub fn serve_static_file(dir : String, url_path : String) -> Response? {
  let full = resolve_static(dir, url_path) catch { _err => return None }
  match full {
    Some(path) => {
      let content_type = content_type_for(path)
      let resp = Response::new()
      resp.set_header("Content-Type", content_type)
      let textual = content_type.has_prefix("text/") ||
        content_type == "application/json" ||
        content_type == "image/svg+xml"
      if textual {
        let text = @fs.read_file_to_string(path) catch { _err => return None }
        resp.set_body(text)
      } else {
        let bytes = @fs.read_file_to_bytes(path) catch { _err => return None }
        resp.set_body_bytes(bytes)
      }
      Some(resp)
    }
    None => None
  }
}