///|
fn require_string(
  j : Json,
  key : String,
  ctx : String,
) -> String raise DeltaError {
  match json_field_string(j, key) {
    Some(s) => s
    None =>
      raise DeltaError::ParseError("\{ctx}: missing string field '\{key}'")
  }
}

///|
fn parse_feature_names(j : Json, key : String) -> Array[String] raise DeltaError {
  let out : Array[String] = []
  match json_field_array(j, key) {
    Some(arr) =>
      for item in arr {
        match json_as_string(item) {
          Some(s) => out.push(s)
          None =>
            raise DeltaError::ParseError(
              "protocol.\{key} contains a non-string feature name",
            )
        }
      }
    None => ()
  }
  out
}

///|
fn parse_protocol(j : Json) -> Protocol raise DeltaError {
  let min_reader = match json_field_int(j, "minReaderVersion") {
    Some(v) => v
    None => 1
  }
  let min_writer = match json_field_int(j, "minWriterVersion") {
    Some(v) => v
    None => 1
  }
  let reader_features = parse_feature_names(j, "readerFeatures")
  let writer_features = parse_feature_names(j, "writerFeatures")
  { min_reader_version: min_reader, min_writer_version: min_writer, reader_features, writer_features, }
}

///|
fn parse_partition_columns(j : Json) -> Array[String] {
  let out : Array[String] = []
  match json_field_array(j, "partitionColumns") {
    Some(arr) =>
      for item in arr {
        match json_as_string(item) {
          Some(s) => out.push(s)
          None => ()
        }
      }
    None => ()
  }
  out
}

///|
pub fn parse_metadata(j : Json) -> TableInfo raise DeltaError {
  let id = match json_field_string(j, "id") {
    Some(s) => s
    None => ""
  }
  let name = json_field_string(j, "name")
  let schema_string = match json_field_string(j, "schemaString") {
    Some(s) => s
    None => raise DeltaError::ParseError("metaData.schemaString is missing")
  }
  let schema = parse_schema(schema_string)
  let partition_columns = parse_partition_columns(j)
  let format_provider = match json_field(j, "format") {
    Some(fmt) =>
      match json_field_string(fmt, "provider") {
        Some(p) => p
        None => "parquet"
      }
    None => "parquet"
  }
  let configuration = match json_field(j, "configuration") {
    Some(cfg) => json_object_pairs_string(cfg)
    None => []
  }
  { id, name, schema, partition_columns, format_provider, configuration, }
}

///|
/// Delta 规范:add/remove.path 是 percent-encoded 的 URI 路径成分。
/// 所有路径先解码为同一规范形式,再用于文件访问或状态匹配。
pub fn decode_action_path(encoded : String) -> String raise DeltaError {
  let bytes = @percent.decode(encoded)
  @utf8.decode(bytes[:]) catch {
    Malformed(_) =>
      raise DeltaError::ParseError(
        "action path is not valid percent-encoded UTF-8: '\{encoded}'",
      )
  }
}

///|
pub fn parse_add(j : Json) -> AddFile raise DeltaError {
  let path = decode_action_path(require_string(j, "path", "add"))
  let partition_values = match json_field(j, "partitionValues") {
    Some(pv) => json_object_pairs_string(pv)
    None => []
  }
  let size = match json_field_int64(j, "size") {
    Some(v) => v
    None => 0L
  }
  let modification_time = match json_field_int64(j, "modificationTime") {
    Some(v) => v
    None => 0L
  }
  let data_change = match json_field_bool(j, "dataChange") {
    Some(b) => b
    None => true
  }
  let stats = match json_field(j, "stats") {
    Some(Json::String(s)) => Some(s)
    Some(_) =>
      raise DeltaError::ParseError("add.stats must be a JSON-encoded string")
    None => None
  }
  let has_deletion_vector = json_field(j, "deletionVector") is Some(_)
  { path, partition_values, size, modification_time, data_change, stats, has_deletion_vector, }
}

///|
pub fn parse_remove(j : Json) -> RemoveFile raise DeltaError {
  let path = decode_action_path(require_string(j, "path", "remove"))
  let deletion_timestamp = json_field_int64(j, "deletionTimestamp")
  let data_change = match json_field_bool(j, "dataChange") {
    Some(b) => b
    None => true
  }
  { path, deletion_timestamp, data_change, }
}

///|
pub fn parse_commit_info(j : Json, version : Int32) -> CommitInfo {
  let timestamp = json_field_int64(j, "timestamp")
  let operation = json_field_string(j, "operation")
  let params = match json_field(j, "operationParameters") {
    Some(p) => json_object_pairs_string(p)
    None => []
  }
  { version, timestamp, operation, operation_parameters: params, }
}

///|
fn parse_txn(j : Json) -> Txn raise DeltaError {
  let app_id = require_string(j, "appId", "txn")
  let version = match json_field_int64(j, "version") {
    Some(v) => v
    None =>
      raise DeltaError::ParseError("txn: missing int64 field 'version'")
  }
  { app_id, version, }
}

///|
/// 解析一行 Delta 日志(一行一个 action)。
/// 已知但与快照无关的 action(cdc / domainMetadata)显式忽略并记录名字;
/// 未识别的 action 直接报错——不静默跳过。
pub fn parse_action(
  line : StringView,
  version : Int32,
) -> Action raise DeltaError {
  let j = @json.parse(line) catch {
    _ =>
      raise DeltaError::ParseError(
        "invalid JSON in _delta_log version \{version}",
      )
  }
  match json_field(j, "protocol") {
    Some(p) => return ProtocolAction(parse_protocol(p))
    None => ()
  }
  match json_field(j, "metaData") {
    Some(m) => return MetaDataAction(parse_metadata(m))
    None => ()
  }
  match json_field(j, "add") {
    Some(a) => return AddAction(parse_add(a))
    None => ()
  }
  match json_field(j, "remove") {
    Some(r) => return RemoveAction(parse_remove(r))
    None => ()
  }
  match json_field(j, "commitInfo") {
    Some(c) => return CommitInfoAction(parse_commit_info(c, version))
    None => ()
  }
  match json_field(j, "txn") {
    Some(t) => return TxnAction(parse_txn(t))
    None => ()
  }
  for known in ["cdc", "domainMetadata"] {
    if json_field(j, known) is Some(_) {
      return IgnoredAction(known)
    }
  }
  let mut unknown = ""
  if json_as_object(j) is Some(obj) {
    for k, _ in obj {
      unknown = k
      break
    }
  }
  raise DeltaError::Unsupported(
    "unknown action '\{unknown}' in _delta_log version \{version} (this kernel implements the action closed set: protocol/metaData/add/remove/commitInfo/txn, with cdc/domainMetadata explicitly ignored)",
  )
}