///|
/// Errors resulting from path resolution and traversal defense.
pub(all) suberror PathError {
  MalformedUri(String) // Maps to 400 Bad Request
  OutsideBaseUrl(String) // Maps to 403 Forbidden with empty body (C042.21)
  TraversalForbidden(String) // Maps to 403 Forbidden
  NotFound(String) // Maps to 404 Not Found
} derive(Eq, Debug)

///|
fn find_char_str(s : String, c : Char) -> Int? {
  let len = s.length()
  let target = c.to_int()
  for i = 0; i < len; i = i + 1 {
    if s[i].to_int() == target {
      return Some(i)
    }
  }
  None
}

///|
fn find_char_view(s : StringView, c : Char) -> Int? {
  let len = s.length()
  let target = c.to_int()
  for i = 0; i < len; i = i + 1 {
    if s[i].to_int() == target {
      return Some(i)
    }
  }
  None
}

///|
/// Validate a user supplied root-relative path.
/// Cleanly permits "" (representing the root directory itself).
pub fn validate_relative_path(path : String) -> Bool {
  if path == "" {
    return true
  }
  if path[0] == '/' || path.contains("\u0000") || path.contains("\\") {
    return false
  }
  let parts = path.split("/").to_array()
  for part in parts {
    if part == ".." {
      return false
    }
    // Reject Windows Alternate Data Streams (ADS) and drive colon
    if find_char_view(part, ':') is Some(_) {
      return false
    }
    // Reject Windows reserved device names
    if is_windows_reserved_name(part) {
      return false
    }
  }
  true
}

///|
/// Check if a path component is a Windows reserved device name.
fn is_windows_reserved_name(part : StringView) -> Bool {
  let dot_idx = find_char_view(part, '.')
  let base = match dot_idx {
    Some(idx) => part[:idx]
    None => part
  }
  let upper = StringBuilder()
  for c in base {
    let cp = c.to_int()
    if cp >= 'a'.to_int() && cp <= 'z'.to_int() {
      upper.write_char(Int::unsafe_to_char(cp - 32))
    } else {
      upper.write_char(c)
    }
  }
  let name = upper.to_string()
  match name {
    "CON"
    | "PRN"
    | "AUX"
    | "NUL"
    | "COM1"
    | "COM2"
    | "COM3"
    | "COM4"
    | "COM5"
    | "COM6"
    | "COM7"
    | "COM8"
    | "COM9"
    | "LPT1"
    | "LPT2"
    | "LPT3"
    | "LPT4"
    | "LPT5"
    | "LPT6"
    | "LPT7"
    | "LPT8"
    | "LPT9" => true
    _ => false
  }
}

///|
/// Validate percent encoding syntax across the entire target string.
/// Ensures malformed percent escapes like `/%` or `/?%` are rejected with 400.
pub fn validate_uri_encoding(target : String) -> Bool {
  let chars = target.to_array()
  let len = chars.length()
  let mut i = 0
  while i < len {
    if chars[i] == '%' {
      if i + 2 >= len {
        return false
      }
      if !is_hex_digit(chars[i + 1].to_int()) ||
        !is_hex_digit(chars[i + 2].to_int()) {
        return false
      }
      i = i + 3
    } else {
      i = i + 1
    }
  }
  true
}

///|
fn is_hex_digit(code : Int) -> Bool {
  (code >= '0'.to_int() && code <= '9'.to_int()) ||
  (code >= 'a'.to_int() && code <= 'f'.to_int()) ||
  (code >= 'A'.to_int() && code <= 'F'.to_int())
}

///|
fn hex_val(code : Int) -> Int {
  if code >= '0'.to_int() && code <= '9'.to_int() {
    code - '0'.to_int()
  } else if code >= 'a'.to_int() && code <= 'f'.to_int() {
    code - 'a'.to_int() + 10
  } else {
    code - 'A'.to_int() + 10
  }
}

///|
/// Decode raw UTF-8 byte stream into a MoonBit String.
fn decode_utf8_bytes(bytes : Array[Int]) -> String? {
  let buf = StringBuilder()
  let len = bytes.length()
  let mut i = 0
  while i < len {
    let b0 = bytes[i]
    if b0 < 0x80 {
      buf.write_char(Int::unsafe_to_char(b0))
      i = i + 1
    } else if b0 >= 0xC2 && b0 <= 0xDF {
      if i + 1 >= len {
        return None
      }
      let b1 = bytes[i + 1]
      if (b1 & 0xC0) != 0x80 {
        return None
      }
      let cp = ((b0 & 0x1F) << 6) | (b1 & 0x3F)
      buf.write_char(Int::unsafe_to_char(cp))
      i = i + 2
    } else if b0 >= 0xE0 && b0 <= 0xEF {
      if i + 2 >= len {
        return None
      }
      let b1 = bytes[i + 1]
      let b2 = bytes[i + 2]
      if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 {
        return None
      }
      let cp = ((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F)
      buf.write_char(Int::unsafe_to_char(cp))
      i = i + 3
    } else if b0 >= 0xF0 && b0 <= 0xF4 {
      if i + 3 >= len {
        return None
      }
      let b1 = bytes[i + 1]
      let b2 = bytes[i + 2]
      let b3 = bytes[i + 3]
      if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80 {
        return None
      }
      let cp = ((b0 & 0x07) << 18) |
        ((b1 & 0x3F) << 12) |
        ((b2 & 0x3F) << 6) |
        (b3 & 0x3F)
      buf.write_char(Int::unsafe_to_char(cp))
      i = i + 4
    } else {
      return None
    }
  }
  Some(buf.to_string())
}

///|
/// Percent-decode a string or slice exactly once, decoding UTF-8 byte sequences.
pub fn decode_percent(value : StringView) -> String raise PathError {
  let len = value.length()
  let bytes : Array[Int] = []
  let mut i = 0
  while i < len {
    if value[i].to_int() == '%'.to_int() {
      if i + 2 >= len ||
        !is_hex_digit(value[i + 1].to_int()) ||
        !is_hex_digit(value[i + 2].to_int()) {
        raise PathError::MalformedUri("malformed percent escape")
      }
      let code = (hex_val(value[i + 1].to_int()) << 4) |
        hex_val(value[i + 2].to_int())
      bytes.push(code)
      i = i + 3
    } else {
      let cp = value[i].to_int()
      if cp < 0x80 {
        bytes.push(cp)
      } else if cp <= 0x7FF {
        bytes.push(0xC0 | (cp >> 6))
        bytes.push(0x80 | (cp & 0x3F))
      } else if cp <= 0xFFFF {
        bytes.push(0xE0 | (cp >> 12))
        bytes.push(0x80 | ((cp >> 6) & 0x3F))
        bytes.push(0x80 | (cp & 0x3F))
      } else {
        bytes.push(0xF0 | (cp >> 18))
        bytes.push(0x80 | ((cp >> 12) & 0x3F))
        bytes.push(0x80 | ((cp >> 6) & 0x3F))
        bytes.push(0x80 | (cp & 0x3F))
      }
      i = i + 1
    }
  }
  match decode_utf8_bytes(bytes) {
    Some(s) => s
    None => raise PathError::MalformedUri("invalid UTF-8 in percent escape")
  }
}

///|
/// Pure path resolution with root anchoring and BaseURL prefix matching.
pub fn resolve_path(
  root : String,
  base_url : String,
  req_target : String,
) -> Result[String, PathError] {
  // 1. Validate full URI encoding syntax (covers `/?%` and `/%`)
  if !validate_uri_encoding(req_target) {
    return Err(PathError::MalformedUri("malformed URI percent encoding"))
  }

  // 2. Separate pathname and query string
  let target_parts = req_target.split("?").to_array()
  let raw_path = target_parts[0]

  // 3. Percent decode pathname once
  let decoded_path = decode_percent(raw_path) catch { e => return Err(e) }

  // 4. Reject NUL bytes in decoded path
  if decoded_path.contains("\u0000") {
    return Err(PathError::TraversalForbidden("null byte in path"))
  }

  // 5. Match BaseURL by complete path components
  let relative : String = match
    match_and_strip_base_url(base_url, decoded_path) {
    Some(rel) => rel
    None => return Err(PathError::OutsideBaseUrl(decoded_path))
  }

  // 6. Validate relative path components (safely handles "")
  if !validate_relative_path(relative) {
    return Err(PathError::TraversalForbidden("path traversal detected"))
  }

  // 7. Normalize root (strip trailing slash if not "/" or drive root)
  let norm_root = if root.length() > 1 &&
    (root.has_suffix("/") || root.has_suffix("\\")) {
    root[:root.length() - 1].to_owned()
  } else {
    root
  }

  // 8. Construct resolved path anchored to root
  let resolved = if relative == "" {
    norm_root
  } else {
    norm_root + "/" + relative
  }

  // 9. Root component boundary assertion (prevents /root-other collision)
  if resolved != norm_root &&
    !resolved.has_prefix(norm_root + "/") &&
    !resolved.has_prefix(norm_root + "\\") {
    return Err(PathError::TraversalForbidden("path escapes root boundary"))
  }

  Ok(resolved)
}

///|
/// Constant-time string comparison preventing timing side-channel attacks.
/// Loop duration depends strictly on `expected.length()`.
pub fn crypto_equals(expected : String, actual : String) -> Bool {
  let exp_len = expected.length()
  let act_len = actual.length()
  let mut diff = exp_len ^ act_len
  let exp_chars = expected.to_array()
  let act_chars = actual.to_array()
  for i = 0; i < exp_len; i = i + 1 {
    let act_char = if i < act_len { act_chars[i].to_int() } else { 0 }
    diff = diff | (exp_chars[i].to_int() ^ act_char)
  }
  diff == 0
}

///|
/// Decode Base64 string to a UTF-8 string. Pure, portable implementation.
pub fn base64_decode_string(input : StringView) -> String? {
  let bytes : Array[Int] = []
  let mut val = 0
  let mut valb = -8
  for c in input {
    let d = base64_char_value(c)
    if d == -1 {
      if c == '=' || c == ' ' || c == '\r' || c == '\n' || c == '\t' {
        continue
      }
      return None
    }
    val = (val << 6) | d
    valb = valb + 6
    if valb >= 0 {
      let byte_val = (val >> valb) & 0xFF
      bytes.push(byte_val)
      valb = valb - 8
    }
  }
  decode_utf8_bytes(bytes)
}

///|
fn base64_char_value(c : Char) -> Int {
  if c >= 'A' && c <= 'Z' {
    c.to_int() - 'A'.to_int()
  } else if c >= 'a' && c <= 'z' {
    c.to_int() - 'a'.to_int() + 26
  } else if c >= '0' && c <= '9' {
    c.to_int() - '0'.to_int() + 52
  } else if c == '+' {
    62
  } else if c == '/' {
    63
  } else {
    -1
  }
}

///|
/// Parse an HTTP Authorization header value: `Basic `.
pub fn parse_basic_auth_header(header : String) -> (String, String)? {
  let trimmed = header.trim()
  if !trimmed.to_owned().to_lower().has_prefix("basic ") {
    return None
  }
  let encoded = trimmed[6:].trim()
  match base64_decode_string(encoded) {
    Some(decoded) => {
      let colon_idx = match find_char_str(decoded, ':') {
        Some(pos) => pos
        None => return None
      }
      let user = decoded[:colon_idx].to_owned()
      let pass = decoded[colon_idx + 1:].to_owned()
      Some((user, pass))
    }
    None => None
  }
}

///|
/// Verify Basic Auth credentials with timing attack safety and dummy fallbacks.
pub fn verify_basic_auth(header : String?, expected : (String, String)) -> Bool {
  match header {
    Some(h) =>
      match parse_basic_auth_header(h) {
        Some((u, p)) => {
          // Unconditionally evaluate BOTH to avoid timing leak on username
          let u_ok = crypto_equals(expected.0, u)
          let p_ok = crypto_equals(expected.1, p)
          u_ok && p_ok
        }
        None => {
          // Dummy calls maintain uniform latency
          ignore(crypto_equals(expected.0, ""))
          ignore(crypto_equals(expected.1, ""))
          false
        }
      }
    None => {
      ignore(crypto_equals(expected.0, ""))
      ignore(crypto_equals(expected.1, ""))
      false
    }
  }
}

///|
/// Extract hostname from a Host header value by stripping port.
pub fn extract_hostname(host_value : String) -> String {
  let trimmed = host_value.trim()
  if trimmed.has_prefix("[") {
    match find_char_view(trimmed, ']') {
      Some(pos) => trimmed[:pos + 1].to_owned()
      None => trimmed.to_owned()
    }
  } else {
    match find_char_view(trimmed, ':') {
      Some(pos) => trimmed[:pos].to_owned()
      None => trimmed.to_owned()
    }
  }
}

///|
/// Check if request Host header is allowed by the whitelist.
pub fn check_host_allowed(
  host_header : String?,
  allowed_hosts : Array[String],
) -> Bool {
  if allowed_hosts.is_empty() {
    return true
  }
  match host_header {
    Some(raw_host) => {
      let hostname = extract_hostname(raw_host).to_lower()
      for allowed in allowed_hosts {
        if allowed.to_lower() == hostname {
          return true
        }
      }
      false
    }
    None => false
  }
}

///|
/// Apply configured security headers (CORS, COOP, PNA, custom headers) to response.
pub fn apply_security_headers(
  headers : Map[String, String],
  config : Config,
  meth : Method,
) -> Unit {
  // 1. CORS Headers
  if config.cors {
    headers["Access-Control-Allow-Origin"] = "*"
    let base_headers = "Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since"
    let allow_headers = match config.cors_headers {
      Some(extra) => base_headers + ", " + extra
      None => base_headers
    }
    headers["Access-Control-Allow-Headers"] = allow_headers
    if meth == Other("OPTIONS") {
      headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS"
    }
  }

  // 2. COOP / COEP Headers
  if config.coop {
    let policy = match config.coop_header {
      Some(h) => h
      None => "same-origin"
    }
    headers["Cross-Origin-Opener-Policy"] = policy
    headers["Cross-Origin-Embedder-Policy"] = "require-corp"
  }

  // 3. Private Network Access (PNA)
  if config.pna {
    headers["Access-Control-Allow-Private-Network"] = "true"
  }

  // 4. Custom configured headers
  for k, v in config.custom_headers {
    headers[k] = v
  }
}

///|
/// Result of preliminary security policy evaluation.
pub(all) enum SecurityDecision {
  Proceed
  Unauthorized(Map[String, String], Bytes)
  Forbidden(Map[String, String], Bytes)
  Preflight(Map[String, String])
} derive(Debug)

///|
/// Execute security policies in the documented priority order:
/// Host Whitelist -> Basic Auth -> OPTIONS Preflight.
pub fn evaluate_security_policies(
  config : Config,
  req : Request,
) -> SecurityDecision {
  // 1. Host Whitelist Check
  if !config.host_whitelist.is_empty() {
    let host_val = find_header_case_insensitive(req.headers, "host")
    if !check_host_allowed(host_val, config.host_whitelist) {
      let headers : Map[String, String] = Map([])
      apply_security_headers(headers, config, req.meth)
      return SecurityDecision::Forbidden(headers, b"Access denied")
    }
  }

  // 2. HTTP Basic Auth Check (strictly precedes file resolution, C042.15)
  match config.basic_auth {
    Some(expected) => {
      let auth_val = find_header_case_insensitive(req.headers, "authorization")
      if !verify_basic_auth(auth_val, expected) {
        let headers : Map[String, String] = Map([
          ("WWW-Authenticate", "Basic realm=\"\""),
        ])
        apply_security_headers(headers, config, req.meth)
        return SecurityDecision::Unauthorized(headers, b"Access denied")
      }
    }
    None => ()
  }

  // 3. OPTIONS Preflight Handling
  if req.meth == Other("OPTIONS") && (config.cors || config.coop) {
    let headers : Map[String, String] = Map([])
    apply_security_headers(headers, config, req.meth)
    return SecurityDecision::Preflight(headers)
  }

  SecurityDecision::Proceed
}

///|
/// Find header in headers map case-insensitively.
pub fn find_header_case_insensitive(
  headers : Map[String, String],
  name : String,
) -> String? {
  let target = name.to_lower()
  for k, v in headers {
    if k.to_lower() == target {
      return Some(v)
    }
  }
  None
}


///|
pub extend PathError with Eq::{not_equal, equal}

///|
pub extend PathError with Debug::{to_repr}

///|
pub extend SecurityDecision with Debug::{to_repr}