///|
/// A small `.env` reader, following the same rules as python-dotenv, which the
/// reference implementation uses.
///
/// Supported: blank lines, `#` comments, an optional `export` prefix,
/// `KEY=VALUE` and bare `KEY`, single-quoted (literal) and double-quoted
/// (escape-aware) values, inline comments after unquoted values, and `$VAR` /
/// `${VAR}` expansion against the variables seen so far.

///|
/// Parse `.env` text. Later definitions override earlier ones; expansion sees
/// the accumulated map.
pub fn parse_dotenv(text : String) -> Map[String, String] {
  let out : Map[String, String] = Map([])
  for raw_line in text.split("\n") {
    let line = strip_dotenv_line(raw_line.to_owned())
    if line.length() == 0 {
      continue
    }
    let (key, value) = match line.split_once("=") {
      Some(pair) => (pair.0.to_owned(), pair.1.to_owned())
      None => (line, "")
    }
    let key = key.trim().to_owned()
    if key.length() == 0 {
      continue
    }
    out[key] = expand_dotenv_value(unquote_dotenv_value(value), out)
  }
  out
}

///|
/// Read and parse a `.env` file, returning an empty map when it does not exist.
pub fn dotenv_to_dict(path : String) -> Map[String, String] {
  if !@fs.path_exists(path) {
    return Map([])
  }
  let text = @fs.read_file_to_string(path) catch { _ => return Map([]) }
  parse_dotenv(text)
}

///|
/// Trim a raw line: drop leading whitespace, an `export ` prefix, trailing
/// whitespace, and a trailing comment that is outside quotes.
fn strip_dotenv_line(raw : String) -> String {
  let mut line = raw.trim_start(chars=" \t").to_owned()
  if line.has_prefix("export ") {
    line = line[7:].to_owned()
  } else if line.has_prefix("export\t") {
    line = line[7:].to_owned()
  }
  line = strip_inline_comment(line).trim_end(chars=" \t").to_owned()
  line
}

///|
/// Drop a ` # comment` tail, unless it is inside quotes.
fn strip_inline_comment(line : String) -> String {
  let chars = line.to_array()
  let mut in_single = false
  let mut in_double = false
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c == '\'' && !in_double {
      in_single = !in_single
    } else if c == '"' && !in_single {
      in_double = !in_double
    } else if c == '#' && !in_single && !in_double {
      if i == 0 || chars[i - 1] == ' ' || chars[i - 1] == '\t' {
        return line[:i].to_owned()
      }
    }
    i += 1
  }
  line
}

///|
/// Remove surrounding quotes from a value, honouring backslash escapes inside
/// double quotes.
fn unquote_dotenv_value(raw : String) -> String {
  let value = raw.trim_start(chars=" \t").to_owned()
  if value.length() >= 2 && value[0] == '"' && value[value.length() - 1] == '"' {
    let inner = value[1:value.length() - 1].to_owned()
    return unescape_double(inner)
  }
  if value.length() >= 2 &&
    value[0] == '\'' &&
    value[value.length() - 1] == '\'' {
    return value[1:value.length() - 1].to_owned()
  }
  value.trim_end(chars=" \t").to_owned()
}

///|
/// Resolve `\n`, `\t`, `\"`, `\\` and `\$` inside a double-quoted value.
fn unescape_double(s : String) -> String {
  let out = StringBuilder(size_hint=s.length())
  let chars = s.to_array()
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '\\' && i + 1 < chars.length() {
      let n = chars[i + 1]
      match n {
        'n' => out.write_char('\n')
        'r' => out.write_char('\r')
        't' => out.write_char('\t')
        '"' => out.write_char('"')
        '\\' => out.write_char('\\')
        '$' => out.write_char('$')
        _ => {
          out.write_char('\\')
          out.write_char(n)
        }
      }
      i += 2
      continue
    }
    out.write_char(chars[i])
    i += 1
  }
  out.to_string()
}

///|
/// Expand `$VAR` and `${VAR}` in a `.env` value against the variables parsed so
/// far. An unknown variable expands to the empty string, matching
/// python-dotenv's default.
fn expand_dotenv_value(value : String, known : Map[String, String]) -> String {
  if !value.contains_char('$') {
    return value
  }
  let out = StringBuilder(size_hint=value.length())
  let chars = value.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c != '$' {
      out.write_char(c)
      i += 1
      continue
    }
    if i + 1 >= chars.length() {
      out.write_char('$')
      i += 1
      continue
    }
    let next = chars[i + 1]
    if next == '$' {
      out.write_char('$')
      i += 2
      continue
    }
    if next == '{' {
      let mut j = i + 2
      let name = StringBuilder()
      while j < chars.length() && chars[j] != '}' {
        name.write_char(chars[j])
        j += 1
      }
      if j < chars.length() {
        out.write_string(known.get(name.to_string()).unwrap_or(""))
        i = j + 1
      } else {
        out.write_char('$')
        i += 1
      }
      continue
    }
    if dotenv_var_start(next) {
      let mut j = i + 1
      let name = StringBuilder()
      while j < chars.length() && dotenv_var_char(chars[j]) {
        name.write_char(chars[j])
        j += 1
      }
      out.write_string(known.get(name.to_string()).unwrap_or(""))
      i = j
      continue
    }
    out.write_char('$')
    i += 1
  }
  out.to_string()
}

///|
fn dotenv_var_start(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}

///|
fn dotenv_var_char(c : Char) -> Bool {
  dotenv_var_start(c) || (c >= '0' && c <= '9')
}