// MoonDNS - Pure MoonBit Authoritative DNS Server
// Copyright 2026 chgttyyr
// Licensed under the Apache-2.0 License.

///|
priv struct ZoneLogicalRecord {
  tokens : Array[String]
  start_line : Int
  owner_omitted : Bool
}

///|
/// Tokenize a logical record while preserving quoted strings (e.g. "v=spf1 ...")
fn tokenize_line(line : String) -> Array[String] {
  let tokens : Array[String] = []
  let mut current = StringBuilder()
  let mut in_quotes = false
  let mut i = 0

  while i < line.length() {
    let ch = line[i].to_int()
    if ch == 59 && !in_quotes { // ';' starts a comment outside quotes
      break
    }
    if ch == 34 { // '"'
      in_quotes = !in_quotes
      current.write_char(Int::unsafe_to_char(ch))
    } else if (ch == 32 || ch == 9) && !in_quotes { // Space or Tab
      if current.to_string().length() > 0 {
        tokens.push(current.to_string())
        current = StringBuilder()
      }
    } else {
      current.write_char(Int::unsafe_to_char(ch))
    }
    i = i + 1
  }

  if current.to_string().length() > 0 {
    tokens.push(current.to_string())
  }
  tokens
}

///|
/// Split Zone input into logical records while tracking source lines and owner indentation
fn scan_logical_records(
  text : String,
) -> Result[Array[ZoneLogicalRecord], @types.DnsError] {
  let lines : Array[String] = []
  let mut line_buf = StringBuilder()
  for i = 0; i < text.length(); i = i + 1 {
    let ch = text[i].to_int()
    if ch == 10 {
      lines.push(line_buf.to_string())
      line_buf = StringBuilder()
    } else if ch != 13 {
      line_buf.write_char(Int::unsafe_to_char(ch))
    }
  }
  if line_buf.to_string().length() > 0 || text.length() == 0 {
    lines.push(line_buf.to_string())
  }

  let records : Array[ZoneLogicalRecord] = []
  let mut logical_buf = StringBuilder()
  let mut in_quotes = false
  let mut paren_depth = 0
  let mut record_start_line = 1
  let mut record_owner_omitted = false
  let mut has_record_content = false

  for line_idx = 0; line_idx < lines.length(); line_idx = line_idx + 1 {
    let line = lines[line_idx]
    let mut line_has_content = false
    let mut line_has_leading_space = false
    let mut i = 0
    while i < line.length() {
      let ch = line[i].to_int()
      if ch == 59 && !in_quotes { // ';' starts a comment outside quotes
        break
      }

      if ch == 32 || ch == 9 {
        if !line_has_content {
          line_has_leading_space = true
        }
        if has_record_content {
          logical_buf.write_char(' ')
        }
        i = i + 1
        continue
      }

      if !has_record_content {
        record_start_line = line_idx + 1
        record_owner_omitted = line_has_leading_space
        has_record_content = true
      }
      line_has_content = true

      if ch == 34 { // '"'
        in_quotes = !in_quotes
        logical_buf.write_char(Int::unsafe_to_char(ch))
      } else if !in_quotes && ch == 40 { // '('
        paren_depth = paren_depth + 1
        logical_buf.write_char(' ')
      } else if !in_quotes && ch == 41 { // ')'
        if paren_depth == 0 {
          return Err(
            @types.ProtocolError(
              "Line " +
              (line_idx + 1).to_string() +
              ": unmatched closing parenthesis",
            ),
          )
        }
        paren_depth = paren_depth - 1
        logical_buf.write_char(' ')
      } else {
        logical_buf.write_char(Int::unsafe_to_char(ch))
      }
      i = i + 1
    }

    if in_quotes {
      return Err(
        @types.ProtocolError(
          "Line " +
          record_start_line.to_string() +
          ": unterminated quoted string",
        ),
      )
    }

    if paren_depth == 0 {
      let tokens = tokenize_line(logical_buf.to_string())
      if tokens.length() > 0 {
        records.push({
          tokens,
          start_line: record_start_line,
          owner_omitted: record_owner_omitted,
        })
      }
      logical_buf = StringBuilder()
      has_record_content = false
      record_owner_omitted = false
    } else {
      logical_buf.write_char(' ')
    }
  }

  if paren_depth != 0 {
    return Err(
      @types.ProtocolError(
        "Line " +
        record_start_line.to_string() +
        ": unterminated parenthesized record",
      ),
    )
  }
  Ok(records)
}

///|
/// Parse an integer string into UInt
fn parse_uint(s : String) -> UInt? {
  let mut val : UInt = 0
  let trimmed = s.trim().to_owned()
  if trimmed.length() == 0 {
    return None
  }
  for i = 0; i < trimmed.length(); i = i + 1 {
    let ch = trimmed[i].to_int()
    if ch >= 48 && ch <= 57 {
      let digit = (ch - 48).reinterpret_as_uint()
      if val > 429496729 || (val == 429496729 && digit > 5) {
        return None
      }
      val = val * 10 + digit
    } else {
      return None
    }
  }
  Some(val)
}

///|
/// Parse a DNS TTL as seconds, accepting compound units such as 1h30m.
fn parse_ttl(s : String) -> UInt? {
  let text = s.trim().to_owned().to_lower()
  if text.length() == 0 {
    return None
  }
  match parse_uint(text) {
    Some(seconds) => return Some(seconds)
    None => ()
  }

  let mut total : UInt = 0
  let mut index = 0
  while index < text.length() {
    let number_start = index
    while index < text.length() {
      let ch = text[index].to_int()
      if ch >= 48 && ch <= 57 {
        index = index + 1
      } else {
        break
      }
    }
    if index == number_start || index >= text.length() {
      return None
    }
    let amount = match parse_uint(text[number_start:index].to_owned()) {
      Some(value) => value
      None => return None
    }
    let factor : UInt = match text[index].to_int() {
      119 => 604800 // w
      100 => 86400 // d
      104 => 3600 // h
      109 => 60 // m
      115 => 1 // s
      _ => return None
    }
    if amount > 4294967295 / factor {
      return None
    }
    let seconds = amount * factor
    if total > 4294967295 - seconds {
      return None
    }
    total = total + seconds
    index = index + 1
  }
  Some(total)
}

///|
/// Convert a decimal field to UInt16 without silently truncating it.
fn parse_uint16(s : String) -> UInt16? {
  match parse_uint(s) {
    Some(value) if value <= 65535 => Some(value.to_uint16())
    _ => None
  }
}

///|
/// Attach a source line to an error produced while parsing one Zone record.
fn zone_line_error(line : Int, message : String) -> @types.DnsError {
  @types.ProtocolError("Line " + line.to_string() + ": " + message)
}

///|
/// Remove the presentation quotes surrounding one DNS character string.
fn unquote_token(token : String) -> String {
  if token.length() >= 2 && token.has_prefix("\"") && token.has_suffix("\"") {
    token[1:token.length() - 1].to_owned()
  } else {
    token
  }
}

///| Resolve a domain token against the current origin:

///| - '@' becomes current origin

///| - relative name 'www' becomes 'www.'

///|
/// - absolute name 'www.example.com.' remains unchanged
fn resolve_name(token : String, origin : String) -> String {
  let t = token.trim().to_owned()
  if t == "@" || t.length() == 0 {
    return origin
  }
  if t.has_suffix(".") {
    return t
  }
  @domain.normalize_fqdn(t + "." + origin)
}

///|
/// Parse a master-file format Zone text into an in-memory Zone database (RFC 1035 Section 5)
pub fn parse_zone_text(
  initial_origin : String,
  text : String,
) -> Result[Zone, @types.DnsError] {
  let apex_origin = @domain.normalize_fqdn(initial_origin)
  let mut current_origin = apex_origin
  let mut current_ttl : UInt = 3600
  let mut last_owner = apex_origin
  let zone = Zone::new(apex_origin, current_ttl)
  let logical_records = match scan_logical_records(text) {
    Ok(records) => records
    Err(error) => return Err(error)
  }

  for logical_record in logical_records {
    let tokens = logical_record.tokens
    if tokens.length() == 0 {
      continue
    }

    // Check directives: $ORIGIN and $TTL
    let first = tokens[0].to_upper()
    if first == "$ORIGIN" {
      if tokens.length() != 2 {
        return Err(
          @types.ProtocolError(
            "Line " +
            logical_record.start_line.to_string() +
            ": $ORIGIN requires exactly one domain",
          ),
        )
      }
      current_origin = resolve_name(tokens[1], current_origin)
      continue
    }
    if first == "$TTL" {
      if tokens.length() != 2 {
        return Err(
          @types.ProtocolError(
            "Line " +
            logical_record.start_line.to_string() +
            ": $TTL requires exactly one value",
          ),
        )
      }
      match parse_ttl(tokens[1]) {
        Some(ttl) => {
          current_ttl = ttl
          zone.default_ttl = ttl
        }
        None =>
          return Err(
            @types.ProtocolError(
              "Line " +
              logical_record.start_line.to_string() +
              ": Invalid $TTL integer",
            ),
          )
      }
      continue
    }

    // Parse standard RR line:  [ttl] [class]  
    let mut token_pos = 0
    let owner : String = if logical_record.owner_omitted {
      // Name omitted, reuse previous owner
      last_owner
    } else {
      let resolved = resolve_name(tokens[0], current_origin)
      last_owner = resolved
      token_pos = token_pos + 1
      resolved
    }

    // Optional TTL
    let mut row_ttl = current_ttl
    if token_pos < tokens.length() {
      match parse_ttl(tokens[token_pos]) {
        Some(ttl_val) => {
          row_ttl = ttl_val
          token_pos = token_pos + 1
        }
        None => ()
      }
    }

    // Optional CLASS (e.g. IN)
    let mut row_class = @types.RClass::IN
    if token_pos < tokens.length() {
      let class_token = tokens[token_pos].to_upper()
      if class_token == "IN" {
        row_class = @types.RClass::IN
        token_pos = token_pos + 1
      } else if class_token == "CH" ||
        class_token == "HS" ||
        class_token == "NONE" ||
        class_token == "ANY" ||
        class_token.has_prefix("CLASS") {
        return Err(
          zone_line_error(
            logical_record.start_line,
            "only the IN class is supported",
          ),
        )
      }
    }

    // Required TYPE
    if token_pos >= tokens.length() {
      return Err(
        zone_line_error(
          logical_record.start_line,
          "resource record is missing a type",
        ),
      )
    }
    let rtype_str = tokens[token_pos].to_upper()
    let rtype = match @types.RType::from_string(rtype_str) {
      Some(t) => t
      None =>
        return Err(
          zone_line_error(
            logical_record.start_line,
            "unsupported RR type " + rtype_str,
          ),
        )
    }
    token_pos = token_pos + 1
    let remaining = tokens.length() - token_pos

    // Parse RDATA according to type
    let rdata : @types.RData = match rtype {
      @types.RType::A => {
        if remaining != 1 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "A record requires exactly one IPv4 address",
            ),
          )
        }
        match @records.parse_ipv4(tokens[token_pos]) {
          Ok(rd) => rd
          Err(e) =>
            return Err(zone_line_error(logical_record.start_line, e.message()))
        }
      }
      @types.RType::AAAA => {
        if remaining != 1 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "AAAA record requires exactly one IPv6 address",
            ),
          )
        }
        match @records.parse_ipv6(tokens[token_pos]) {
          Ok(rd) => rd
          Err(e) =>
            return Err(zone_line_error(logical_record.start_line, e.message()))
        }
      }
      @types.RType::CNAME => {
        if remaining != 1 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "CNAME record requires exactly one target domain",
            ),
          )
        }
        let target = resolve_name(tokens[token_pos], current_origin)
        @types.RData::CNAME(target)
      }
      @types.RType::NS => {
        if remaining != 1 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "NS record requires exactly one server domain",
            ),
          )
        }
        let target = resolve_name(tokens[token_pos], current_origin)
        @types.RData::NS(target)
      }
      @types.RType::PTR => {
        if remaining != 1 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "PTR record requires exactly one target domain",
            ),
          )
        }
        @types.RData::PTR(resolve_name(tokens[token_pos], current_origin))
      }
      @types.RType::MX => {
        if remaining != 2 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "MX record requires preference and exchange",
            ),
          )
        }
        let pref = match parse_uint16(tokens[token_pos]) {
          Some(v) => v
          None =>
            return Err(
              zone_line_error(
                logical_record.start_line,
                "invalid MX preference (expected 0..65535)",
              ),
            )
        }
        let exch = resolve_name(tokens[token_pos + 1], current_origin)
        @types.RData::MX(pref, exch)
      }
      @types.RType::TXT => {
        if remaining == 0 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "TXT record requires at least one character string",
            ),
          )
        }
        let chunks : Array[String] = []
        let mut rdata_length = 0
        while token_pos < tokens.length() {
          let chunk = unquote_token(tokens[token_pos])
          let octets = @utf8.encode(chunk).length()
          if octets > 255 {
            return Err(
              zone_line_error(
                logical_record.start_line,
                "TXT character-string exceeds 255 octets",
              ),
            )
          }
          if rdata_length > 65535 - octets - 1 {
            return Err(
              zone_line_error(
                logical_record.start_line,
                "TXT RDATA exceeds 65535 octets",
              ),
            )
          }
          rdata_length = rdata_length + octets + 1
          chunks.push(chunk)
          token_pos = token_pos + 1
        }
        @types.RData::TXT(chunks)
      }
      @types.RType::SOA => {
        if remaining != 7 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "SOA record requires exactly 7 arguments",
            ),
          )
        }
        let mname = resolve_name(tokens[token_pos], current_origin)
        let rname = resolve_name(tokens[token_pos + 1], current_origin)
        let serial = match parse_uint(tokens[token_pos + 2]) {
          Some(v) => v
          None =>
            return Err(
              zone_line_error(logical_record.start_line, "invalid SOA serial"),
            )
        }
        let refresh = match parse_uint(tokens[token_pos + 3]) {
          Some(v) => v
          None =>
            return Err(
              zone_line_error(logical_record.start_line, "invalid SOA refresh"),
            )
        }
        let retry = match parse_uint(tokens[token_pos + 4]) {
          Some(v) => v
          None =>
            return Err(
              zone_line_error(logical_record.start_line, "invalid SOA retry"),
            )
        }
        let expire = match parse_uint(tokens[token_pos + 5]) {
          Some(v) => v
          None =>
            return Err(
              zone_line_error(logical_record.start_line, "invalid SOA expire"),
            )
        }
        let minimum = match parse_uint(tokens[token_pos + 6]) {
          Some(v) => v
          None =>
            return Err(
              zone_line_error(logical_record.start_line, "invalid SOA minimum"),
            )
        }
        @types.RData::SOA(mname, rname, serial, refresh, retry, expire, minimum)
      }
      @types.RType::SRV => {
        if remaining != 4 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "SRV record requires priority, weight, port, and target",
            ),
          )
        }
        let priority = match parse_uint16(tokens[token_pos]) {
          Some(value) => value
          None =>
            return Err(
              zone_line_error(
                logical_record.start_line,
                "invalid SRV priority (expected 0..65535)",
              ),
            )
        }
        let weight = match parse_uint16(tokens[token_pos + 1]) {
          Some(value) => value
          None =>
            return Err(
              zone_line_error(
                logical_record.start_line,
                "invalid SRV weight (expected 0..65535)",
              ),
            )
        }
        let port = match parse_uint16(tokens[token_pos + 2]) {
          Some(value) => value
          None =>
            return Err(
              zone_line_error(
                logical_record.start_line,
                "invalid SRV port (expected 0..65535)",
              ),
            )
        }
        @types.RData::SRV(
          priority,
          weight,
          port,
          resolve_name(tokens[token_pos + 3], current_origin),
        )
      }
      @types.RType::CAA => {
        if remaining != 3 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "CAA record requires flags, tag, and value",
            ),
          )
        }
        let flags = match parse_uint(tokens[token_pos]) {
          Some(value) if value <= 255 => value.to_byte()
          _ =>
            return Err(
              zone_line_error(
                logical_record.start_line,
                "invalid CAA flags (expected 0..255)",
              ),
            )
        }
        let tag = unquote_token(tokens[token_pos + 1])
        let value = unquote_token(tokens[token_pos + 2])
        let tag_octets = @utf8.encode(tag).length()
        let value_octets = @utf8.encode(value).length()
        if tag_octets == 0 || tag_octets > 255 {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "CAA tag must contain 1..255 octets",
            ),
          )
        }
        if value_octets > 65535 - 2 - tag_octets {
          return Err(
            zone_line_error(
              logical_record.start_line,
              "CAA RDATA exceeds 65535 octets",
            ),
          )
        }
        @types.RData::CAA(flags, tag, value)
      }
      _ =>
        return Err(
          zone_line_error(
            logical_record.start_line,
            "RR type " + rtype_str + " is not supported by the Zone parser",
          ),
        )
    }

    let record = @types.Record::new(owner, rtype, row_class, row_ttl, rdata)
    zone.add_record(record)
  }

  Ok(zone)
}