///|
/// Configuration validation error hierarchy.
/// All errors are detected and raised during pre-listen startup.
pub(all) suberror ConfigError {
  InvalidBaseUrl(String)
  InvalidRoot(String)
  InvalidPort(String)
  InvalidTryFiles(String)
  ConflictingBaseUrl(String)
  ConflictingRouting(String)
  InvalidProxy(String)
  InvalidAuth(String)
  InvalidHeader(String)
  InvalidTimeout(String)
  InvalidTls(String)
  InvalidLimits(String)
} derive(Eq, Debug)

///|
pub fn ConfigError::to_string(self : ConfigError) -> String {
  match self {
    InvalidBaseUrl(s) => "InvalidBaseUrl: " + s
    InvalidRoot(s) => "InvalidRoot: " + s
    InvalidPort(s) => "InvalidPort: " + s
    InvalidTryFiles(s) => "InvalidTryFiles: " + s
    ConflictingBaseUrl(s) => "ConflictingBaseUrl: " + s
    ConflictingRouting(s) => "ConflictingRouting: " + s
    InvalidProxy(s) => "InvalidProxy: " + s
    InvalidAuth(s) => "InvalidAuth: " + s
    InvalidHeader(s) => "InvalidHeader: " + s
    InvalidTimeout(s) => "InvalidTimeout: " + s
    InvalidTls(s) => "InvalidTls: " + s
    InvalidLimits(s) => "InvalidLimits: " + s
  }
}

///|
pub impl Show for ConfigError with fn output(self, logger) {
  logger.write_string(self.to_string())
}

///|
/// Normalized static-server configuration model.
/// Contains all configuration settings for protocol, static resolution,
/// routing mounts, SPA fallback, security policies, and upstream proxies.
pub(all) struct Config {
  limits : RuntimeLimits
  // --- Filesystem & Network ---
  root : String // Filesystem root directory (default "." or "public")
  port : Int // TCP listen port (0..65535, 0 = ephemeral)
  address : String // Bind address ("0.0.0.0", "::", etc.)

  // --- Routing & Mounts ---
  base_url : String // Normalized URL mount prefix (e.g. "/", "/app")
  default_ext : String? // Default file extension completion (e.g. Some("html"))
  spa : Bool // SPA fallback to root index.html
  try_files : String? // Custom fallback file relative to root

  // --- Compression ---
  gzip : Bool // Detect & serve pre-compressed .gz files
  brotli : Bool // Detect & serve pre-compressed .br files
  force_content_encoding : Bool // Force Content-Encoding for files ending in .gz/.br

  // --- Directory Handling ---
  auto_index : Bool // Serve index.html when requesting a directory
  show_dir : Bool // Render HTML directory listing if no index file
  show_dotfiles : Bool // Include hidden/dotfiles in directory listing
  dir_overrides_404 : Bool // Directory listing takes precedence over custom 404.html

  // --- Caching & ETag ---
  cache_seconds : Int // Cache duration in seconds (-1 = no-cache, no-store)
  cache_control : String? // Explicit Cache-Control header override
  weak_etags : Bool // Generate weak ETags (W/"...")
  weak_compare : Bool // Compare ETags using weak comparison

  // --- Security & Headers ---
  cors : Bool // Enable CORS headers (Access-Control-Allow-Origin: *)
  cors_headers : String? // Custom Access-Control-Allow-Headers
  coop : Bool // Cross-Origin-Opener-Policy: same-origin
  coop_header : String? // Custom Cross-Origin-Opener-Policy value
  pna : Bool // Access-Control-Allow-Private-Network: true
  basic_auth : (String, String)? // (username, password) for HTTP Basic Auth
  host_whitelist : Array[String] // Allowed Host header values (empty = allow all)
  custom_headers : Map[String, String] // User-defined headers injected into all responses
  robots : Bool // Serve default User-agent: *\nDisallow: / for /robots.txt

  // --- Proxy & Upstream ---
  proxy : String? // Upstream proxy target (e.g. "http://127.0.0.1:3000")
  proxy_all : String? // Proxy all requests upstream (requires proxy to be set)
  proxy_options : Map[String, String] // Upstream proxy options (must be empty if fallback active)
  websocket : Bool // Enable WebSocket proxy upgrade capability (default false)
  proxy_config : String? // Upstream proxy config file or route rules

  // --- TLS (D-08) ---
  // TLS is disabled by default (D-01); setting cert_file and key_file
  // together enables HTTPS serving. ca_file/secure configure client-side
  // upstream verification (consumed by T-013 proxy work).
  cert_file : String? // TLS certificate chain file (PEM/DER), leaf first
  key_file : String? // TLS private key file (PEM/DER)
  key_passphrase : String? // Private key passphrase for encrypted PEM keys
  ca_file : String? // Trust-root CA bundle file (PEM) for upstream clients
  secure : Bool // Verify upstream certificates and hostname (default true)

  // --- Lifecycle & Error Handling ---
  handle_error : Bool // If false, delegates 404/errors to host (middleware Next)
  idle_timeout_ms : Int // Socket idle timeout in ms (0 = disabled, default 120000)
  mime_types : Map[String, String] // Custom extension -> MIME type overrides

  // --- Logging & Output ---
  log_ip : Bool // Enable logging of client IP address (default false)
  silent : Bool // Suppress console log messages (default false)
} derive(Debug)

///|
/// Build Server & CLI defaults for a filesystem root directory.
pub fn Config::default(root : String) -> Config {
  {
    root,
    limits: RuntimeLimits::default(),
    port: 8080,
    address: "0.0.0.0",
    base_url: "/",
    default_ext: Some("html"),
    spa: false,
    try_files: None,
    gzip: false, // Server/CLI default is false per AD-02
    brotli: false,
    force_content_encoding: false,
    auto_index: true,
    show_dir: true,
    show_dotfiles: false,
    dir_overrides_404: false,
    cache_seconds: 3600,
    cache_control: None,
    weak_etags: true,
    weak_compare: true,
    cors: false,
    cors_headers: None,
    coop: false,
    coop_header: None,
    pna: false,
    basic_auth: None,
    host_whitelist: [],
    custom_headers: Map([]),
    robots: false,
    proxy: None,
    proxy_all: None,
    proxy_options: Map([]),
    websocket: false,
    proxy_config: None,
    cert_file: None,
    key_file: None,
    key_passphrase: None,
    ca_file: None,
    secure: true,
    handle_error: true,
    idle_timeout_ms: 120000, // 120 seconds per AD-03
    mime_types: Map([]),
    log_ip: false,
    silent: false,
  }
}

///|
/// Build Core Middleware defaults for a filesystem root directory (AD-02 compatibility).
pub fn Config::middleware_default(root : String) -> Config {
  {
    ..Config::default(root),
    gzip: true, // Core middleware default is true per AD-02
  }
}

///|
/// Determine active page fallback mode.
pub fn Config::fallback_mode(self : Config) -> FallbackMode {
  if self.spa {
    FallbackMode::Spa
  } else {
    match self.try_files {
      Some(f) => FallbackMode::TryFiles(f)
      None => FallbackMode::None
    }
  }
}

///|
/// Returns whether any fallback mode is enabled.
pub fn Config::has_fallback(self : Config) -> Bool {
  self.spa || self.try_files is Some(_)
}

///|
/// Returns whether any upstream proxy mode is configured.
pub fn Config::has_proxy(self : Config) -> Bool {
  self.proxy is Some(_) ||
  self.proxy_all is Some(_) ||
  self.proxy_config is Some(_) ||
  !self.proxy_options.is_empty()
}

///|
/// Returns whether WebSocket proxy capability is enabled.
pub fn Config::has_websocket_proxy(self : Config) -> Bool {
  self.has_proxy() && self.websocket
}

///|
/// Returns whether TLS serving is enabled (D-01: disabled by default).
pub fn Config::has_tls(self : Config) -> Bool {
  self.cert_file is Some(_) || self.key_file is Some(_)
}

///|
/// Return whether configured upstream targets may require a TLS client.
/// Inline/file proxy rules are conservatively treated as HTTPS when their
/// configuration contains an `https://` target.
pub fn Config::proxy_targets_include_https(self : Config) -> Bool {
  if self.proxy is Some(url) && (url.has_prefix("https://") || url.has_prefix("wss://")) {
    return true
  }
  if self.proxy_all is Some(url) && (url.has_prefix("https://") || url.has_prefix("wss://")) {
    return true
  }
  match self.proxy_config {
    Some(value) => value.contains("https://") || value.contains("wss://")
    None => false
  }
}

///|
/// Validate TLS configuration pairing: enabling TLS requires both a
/// certificate and a key (D-01). File readability and key/cert matching are
/// verified during server startup preflight when the acceptor is built,
/// before the listener is created.
pub fn validate_tls(config : Config) -> Unit raise ConfigError {
  match (config.cert_file, config.key_file) {
    (Some(_), Some(_)) => ()
    (Some(_), None) =>
      raise ConfigError::InvalidTls("cert_file requires key_file to be set")
    (None, Some(_)) =>
      raise ConfigError::InvalidTls("key_file requires cert_file to be set")
    (None, None) => ()
  }
}

///|
/// Returns the relative file path to serve on fallback, if enabled.
pub fn Config::fallback_target_file(self : Config) -> String? {
  if self.spa {
    Some("index.html")
  } else {
    self.try_files
  }
}

///|
/// Returns the effective Cache-Control header string.
pub fn Config::effective_cache_control(self : Config) -> String {
  match self.cache_control {
    Some(custom) => custom
    None =>
      if self.cache_seconds < 0 {
        "no-cache, no-store, must-revalidate"
      } else {
        "max-age=" + self.cache_seconds.to_string()
      }
  }
}

///|
/// Validate a filesystem root directory path.
pub fn validate_root(root : String) -> String raise ConfigError {
  if root == "" || root.contains("\u0000") {
    raise ConfigError::InvalidRoot("root cannot be empty or contain null bytes")
  }
  root
}

///|
/// Validate a user-specified try_files relative path.
pub fn validate_try_files_path(path : String) -> String raise ConfigError {
  if path == "" {
    raise ConfigError::InvalidTryFiles("try_files path cannot be empty")
  }
  if path[0] == '/' {
    raise ConfigError::InvalidTryFiles(
      "try_files path must be relative to root, cannot start with '/'",
    )
  }
  if path.contains("\u0000") ||
    path.contains("\\") ||
    path.contains("\r") ||
    path.contains("\n") {
    raise ConfigError::InvalidTryFiles(
      "try_files path contains invalid characters",
    )
  }
  if path.contains("$uri") ||
    path.contains("=404") ||
    path.contains(" ") ||
    path.contains(",") {
    raise ConfigError::InvalidTryFiles(
      "try_files does not support Nginx multi-candidate or variable syntax",
    )
  }
  let parts = path.split("/").to_array()
  for part in parts {
    if part == ".." || part == "." {
      raise ConfigError::InvalidTryFiles(
        "try_files path cannot contain dot segments",
      )
    }
    if part == "" {
      raise ConfigError::InvalidTryFiles(
        "try_files path cannot contain consecutive slashes",
      )
    }
  }
  path
}

///|
/// Validate upstream proxy URL format (http:// or https:// with valid host/port).
pub fn validate_proxy_url(url : String) -> Unit raise ConfigError {
  let url=if url.has_prefix("wss://") { "https://"+url[6:].to_owned() }
    else if url.has_prefix("ws://") { "http://"+url[5:].to_owned() } else { url }
  if !url.has_prefix("http://") && !url.has_prefix("https://") {
    raise ConfigError::InvalidProxy(
      "proxy URL must start with http:// or https://",
    )
  }
  let prefix_len = if url.has_prefix("https://") { 8 } else { 7 }
  let rest = url[prefix_len:].to_owned()
  let auth_end = match rest.find("/") {
    Some(idx) => idx
    None =>
      match rest.find("?") {
        Some(idx) => idx
        None => rest.length()
      }
  }
  let authority = rest[:auth_end].to_owned()
  match authority.find(":") {
    Some(colon_idx) => {
      let port_str = authority[colon_idx + 1:].to_owned()
      if port_str.length() == 0 {
        raise ConfigError::InvalidProxy(
          "proxy URL missing port number after ':'",
        )
      }
      let mut p = 0
      for c in port_str {
        if c < '0' || c > '9' {
          raise ConfigError::InvalidProxy(
            "proxy URL has invalid non-numeric port: " + port_str,
          )
        }
        p = p * 10 + (c.to_int() - '0'.to_int())
        if p > 65535 {
          raise ConfigError::InvalidProxy(
            "proxy URL port exceeds 65535: " + port_str,
          )
        }
      }
    }
    None => ()
  }
}

///|
/// Validate all configuration settings before creating any network listener.
/// Raises ConfigError on any syntactic defect or mutual exclusion conflict.
pub fn validate_config(config : Config) -> Unit raise ConfigError {
  config.limits.validate()
  ignore(validate_root(config.root))
  ignore(normalize_base_url(config.base_url))

  if config.port < 0 || config.port > 65535 {
    raise ConfigError::InvalidPort("port must be between 0 and 65535")
  }

  if config.idle_timeout_ms < 0 {
    raise ConfigError::InvalidTimeout("idle_timeout_ms cannot be negative")
  }

  match config.try_files {
    Some(tf) => ignore(validate_try_files_path(tf))
    None => ()
  }

  if config.spa && config.try_files is Some(_) {
    raise ConfigError::ConflictingRouting(
      "cannot specify both --spa and --try-files (mutual exclusion violation)",
    )
  }

  let has_fallback = config.spa || config.try_files is Some(_)
  let has_proxy = config.proxy is Some(_) ||
    config.proxy_all is Some(_) ||
    config.proxy_config is Some(_) ||
    !config.proxy_options.is_empty()

  if has_fallback && has_proxy {
    raise ConfigError::ConflictingRouting(
      "cannot combine page fallback (--spa or --try-files) with proxy configuration",
    )
  }

  if config.proxy_all is Some(_) && config.proxy is None {
    raise ConfigError::InvalidProxy(
      "--proxy-all requires --proxy to be configured",
    )
  }
  match config.proxy {
    Some(p) => validate_proxy_url(p)
    None => ()
  }

  match config.basic_auth {
    Some((username, password)) =>
      if username.contains(":") ||
        username.contains("\u0000") ||
        password.contains("\u0000") {
        raise ConfigError::InvalidAuth(
          "username cannot contain ':' or null bytes",
        )
      }
    None => ()
  }

  if config.cache_seconds < -1 {
    raise ConfigError::InvalidHeader("cache_seconds cannot be less than -1")
  }

  for k, v in config.custom_headers {
    if k.contains("\r") ||
      k.contains("\n") ||
      v.contains("\r") ||
      v.contains("\n") {
      raise ConfigError::InvalidHeader(
        "custom headers cannot contain CRLF characters",
      )
    }
  }

  validate_tls(config)
}

///|
/// Method syntax for validating a Config instance.
pub fn Config::validate(self : Config) -> Unit raise ConfigError {
  validate_config(self)
}

///|
/// Helper to parse CLI basic auth credentials in format "username:password".
pub fn parse_auth_credential(str : String) -> (String, String)? {
  match str.find(":") {
    Some(idx) => {
      let username = str[:idx].to_owned()
      let password = str[idx + 1:].to_owned()
      Some((username, password))
    }
    None => None
  }
}


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

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

///|
pub extend ConfigError with Show::{output}

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