///|
/// PostgreSQL connection configuration.
///
/// Two connection-string formats are accepted, mirroring libpq's `fe-connect.c`:
///
/// **URI** (parsed via `@url.parse`):
/// ```
/// postgresql://[user[:password]@][host][:port][/dbname][?param=value&...]
/// ```
///
/// **Keyword/Value**:
/// ```
/// host=localhost port=5432 user=alice dbname=mydb password=secret
/// ```
/// Values may be single-quoted (`'has spaces'`) and use backslash escaping.
pub(all) struct Config {
  host : String
  hostaddr : String?
  port : Int
  user : String
  database : String?
  password : String?
  sslmode : String
  sslrootcert : String?
  sslcert : String?
  sslkey : String?
  application_name : String?
  connect_timeout : Int
  statement_timeout : Int
  target_session_attrs : String
  trace : Bool
}

///|
/// The default PostgreSQL TCP port.
pub const DEFAULT_PORT : Int = 5432

///|
/// Create a `Config` with explicit parameter values.
pub fn Config::new(
  user : String,
  host? : String = "localhost",
  hostaddr? : String? = None,
  port? : Int = DEFAULT_PORT,
  database? : String? = None,
  password? : String? = None,
  sslmode? : String = "disable",
  sslrootcert? : String? = None,
  sslcert? : String? = None,
  sslkey? : String? = None,
  application_name? : String? = None,
  connect_timeout? : Int = 0,
  statement_timeout? : Int = 0,
  target_session_attrs? : String = "any",
  trace? : Bool = false,
) -> Config {
  {
    host,
    hostaddr,
    port,
    user,
    database,
    password,
    sslmode,
    sslrootcert,
    sslcert,
    sslkey,
    application_name,
    connect_timeout,
    statement_timeout,
    target_session_attrs,
    trace,
  }
}

///|
/// Parse a PostgreSQL connection string.
///
/// Auto-detects URI (`postgresql://` / `postgres://`) vs keyword/value format.
pub fn Config::from_connstr(s : String) -> Config raise WireError {
  if s.has_prefix("postgresql://") || s.has_prefix("postgres://") {
    parse_uri(s)
  } else {
    parse_kv(s)
  }
}

// ---------------------------------------------------------------------------
// URI parsing — delegates to @url.parse
// ---------------------------------------------------------------------------

///|
/// Parse a URI connection string via `@url.parse`.
fn parse_uri(uri_str : String) -> Config raise WireError {
  let url = @url.parse(uri_str) catch {
    _ => raise WireError::Parse("invalid PostgreSQL connection URI: \{uri_str}")
  }

  // user : password
  let (user, password) = match url.user_info {
    Some(info) => (info.username, info.password)
    None => ("", None)
  }

  // host : port  (url.host is the full "host:port" string)
  let (host, port) = match url.host {
    Some(h) => split_hostport(h)
    None => ("localhost", DEFAULT_PORT)
  }

  // database name from path (strip leading /)
  let database : String? = match url.path {
    Some(p) => {
      let trimmed = if p.has_prefix("/") { p[1:].to_owned() } else { p }
      if trimmed.length() > 0 {
        Some(trimmed)
      } else {
        None
      }
    }
    None => None
  }

  // Build from URI components
  let mut config = Config::new(user, host~, port~, database~, password~)

  // Apply query-string overrides (sslmode, application_name, etc.)
  match url.query {
    Some(q) =>
      for pair in q.split("&") {
        match pair.find("=") {
          Some(eq) => {
            let key = pair[0:eq].to_owned()
            let val = pair[eq + 1:].to_owned()
            config = with_param(config, key, val)
          }
          None => ()
        }
      }
    None => ()
  }

  config
}

///|
/// Split `host:port` into `(host, port)`. Handles IPv6 `[::1]:5432`.
fn split_hostport(hostport : String) -> (String, Int) {
  // IPv6: [address]:port
  if hostport.has_prefix("[") {
    match hostport.find("]") {
      Some(close) => {
        let host = hostport[0:close + 1].to_owned()
        let after = hostport[close + 1:]
        let port = if after.has_prefix(":") {
          parse_port(after[1:].to_owned())
        } else {
          DEFAULT_PORT
        }
        return (host, port)
      }
      None => return (hostport, DEFAULT_PORT)
    }
  }
  // Plain host:port
  match hostport.rev_find(":") {
    Some(i) => {
      let h = hostport[0:i].to_owned()
      let p = parse_port(hostport[i + 1:].to_owned())
      (h, p)
    }
    None => (hostport, DEFAULT_PORT)
  }
}

// ---------------------------------------------------------------------------
// Keyword/Value parsing
// ---------------------------------------------------------------------------

///|
/// Parse a keyword/value connection string.
///
/// Format: `key=value [key=value ...]`
///
/// Values may be **bare**, **single-quoted** (`'...'`), or use **backslash
/// escaping**.  Inside single quotes `\'` embeds a literal single quote.
fn parse_kv(s : String) -> Config raise WireError {
  let mut config = Config::new("")
  for s0 = s.to_string_view() {
    match s0 {
      [' ' | '\t' | '\n' | '\r', .. rest] => continue rest
      [] => break
      _ => {
        let (key, after_eq) = scan_keyword(s0)
        let (value, rest) = scan_value(after_eq)
        config = with_param(config, key, value)
        continue rest
      }
    }
  }
  // Sensible defaults for required fields.
  if config.user == "" {
    config = { ..config, user: "postgres" }
  }
  if config.host == "" {
    config = { ..config, host: "localhost" }
  }
  config
}

///|
/// Scan a keyword until `=`.  Handles optional whitespace before `=`.
///
/// Returns `(keyword, rest_past_=)`.
fn scan_keyword(s : StringView) -> (String, StringView) raise WireError {
  guard s.find("=") is Some(index) else {
    raise WireError::Parse(
      "invalid connection string: expected '=' after '\{s}'",
    )
  }
  // Skip optional whitespace between keyword and `=`.
  let key = s[0:index].trim().to_owned()

  (key, s[index + 1:])
}

///|
/// Dispatch to the appropriate value scanner.
fn scan_value(s : StringView) -> (String, StringView) raise WireError {
  match s {
    ['\'', ..] => scan_quoted(s)
    _ => scan_bare(s)
  }
}

///|
/// Scan a single-quoted value.  `s` starts with the opening `'`.
///
/// Backslash escapes the next character (`\'` → `'`, `\\` → `\`).
fn scan_quoted(s : StringView) -> (String, StringView) raise WireError {
  let buf = StringBuilder::new()
  for rest = s[1:] {
    match rest {
      ['\'', .. rest] => return (buf.to_string(), rest)
      [] => raise WireError::Parse("unterminated quoted value")
      ['\\', c, .. rest] => {
        buf.write_char(c)
        continue rest
      }
      [c, .. rest] => {
        buf.write_char(c)
        continue rest
      }
    }
  }
}

///|
/// Scan a bare (unquoted) value until whitespace or end.
///
/// Backslash escapes the next character.
fn scan_bare(s : StringView) -> (String, StringView) {
  let buf = StringBuilder::new()
  for rest = s {
    match rest {
      [' ' | '\t' | '\n' | '\r', .. rest] => return (buf.to_string(), rest)
      [] => return (buf.to_string(), rest)
      ['\\', c, .. r] => {
        buf.write_char(c)
        continue r
      }
      [c, .. r] => {
        buf.write_char(c)
        continue r
      }
    }
  }
}

// ---------------------------------------------------------------------------
// Parameter application
// ---------------------------------------------------------------------------

///|
/// Apply a `(key, value)` pair, returning an updated `Config`.
///
/// Keywords are matched case-insensitively (as in libpq). Unknown keywords
/// are silently ignored.
fn with_param(config : Config, key : String, value : String) -> Config {
  match key.to_lower() {
    "host" => { ..config, host: value }
    "hostaddr" => { ..config, hostaddr: Some(value) }
    "port" => { ..config, port: parse_port(value) }
    "user" | "username" => { ..config, user: value }
    "dbname" | "database" => { ..config, database: Some(value) }
    "password" => { ..config, password: Some(value) }
    "sslmode" => { ..config, sslmode: value }
    "sslrootcert" => { ..config, sslrootcert: Some(value) }
    "sslcert" => { ..config, sslcert: Some(value) }
    "sslkey" => { ..config, sslkey: Some(value) }
    "application_name" => { ..config, application_name: Some(value) }
    "connect_timeout" => { ..config, connect_timeout: parse_int(value) }
    "statement_timeout" => { ..config, statement_timeout: parse_int(value) }
    "target_session_attrs" => { ..config, target_session_attrs: value }
    _ => config
  }
}

///|
/// Parse an integer, returning 0 on failure (sensible default for timeouts).
fn parse_int(s : String) -> Int {
  @string.parse_int(s) catch {
    _ => 0
  }
}

///|
/// Parse a port string, falling back to `DEFAULT_PORT`.
fn parse_port(s : String) -> Int {
  @string.parse_int(s) catch {
    _ => DEFAULT_PORT
  }
}