///|
pub(all) enum ScopedRuleSetV4 {
  Adya2000Mixed
  ElleFixed4e359e5
} derive(Eq)

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

///|
pub(all) enum ScopedCategoryV4 {
  ScopedG0
  ScopedG1c
  ScopedG2Item
  ScopedG2
  ScopedGSingleItem
  ScopedGSingle
  ScopedGNonadjacent
} derive(Eq)

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

///|
pub(all) enum ScopedOrderContextV4 {
  ScopedDataOnly
  ScopedWithProcess
  ScopedWithRealtime
} derive(Eq)

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

///|
pub(all) enum ScopedDataKindV4 {
  ScopedWWItem
  ScopedWWPredicate
  ScopedWRItem
  ScopedWRPredicate
  ScopedRWItem
  ScopedRWPredicate
} derive(Eq)

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

///|
pub(all) enum ScopedOrderKindV4 {
  ScopedProcess
  ScopedRealtime
} derive(Eq)

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

///|
pub(all) struct ScopedDataEdgeInputV4 {
  id : String
  from : String
  to : String
  kind : ScopedDataKindV4
  sources : Array[Json]
}

///|
pub(all) struct ScopedOriginalRefV4 {
  transaction : String
  observation : Json
}

///|
/// Order is supplied explicitly. A physical pooled connection is not a process.
pub(all) struct ScopedOrderEdgeInputV4 {
  id : String
  from : String
  to : String
  kind : ScopedOrderKindV4
  context_id : String
  from_source : ScopedOriginalRefV4
  to_source : ScopedOriginalRefV4
}

///|
priv struct ScopedIndexedEdgeV4 {
  id : String
  from : Int32
  to : Int32
  data_kind : ScopedDataKindV4?
  order_kind : ScopedOrderKindV4?
  original : Json
}

///|
pub struct ScopedDependencyGraphV4 {
  priv nodes : Array[String]
  priv edges : Array[ScopedIndexedEdgeV4]
  priv adjacency : Array[Array[Int32]]
  priv input_record : Json
  priv input_sha256 : String
  priv data_complete : Bool
  priv process_complete : Bool
  priv realtime_complete : Bool
  priv data_gaps : Array[Json]
  priv process_gaps : Array[Json]
  priv realtime_gaps : Array[Json]
}

///|
pub fn ScopedDependencyGraphV4::input_sha256(
  self : ScopedDependencyGraphV4,
) -> String {
  self.input_sha256
}

///|
fn scoped_ruleset_name_v4(value : ScopedRuleSetV4) -> String {
  match value {
    Adya2000Mixed => "adya-2000-scoped-categories-v1"
    ElleFixed4e359e5 => "elle-4e359e5-scoped-categories-v1"
  }
}

///|
fn scoped_category_name_v4(value : ScopedCategoryV4) -> String {
  match value {
    ScopedG0 => "G0"
    ScopedG1c => "G1c"
    ScopedG2Item => "G2-item"
    ScopedG2 => "G2"
    ScopedGSingleItem => "G-single-item"
    ScopedGSingle => "G-single"
    ScopedGNonadjacent => "G-nonadjacent"
  }
}

///|
fn scoped_context_name_v4(value : ScopedOrderContextV4) -> String {
  match value {
    ScopedDataOnly => "data-only"
    ScopedWithProcess => "process"
    ScopedWithRealtime => "realtime"
  }
}

///|
fn scoped_data_name_v4(value : ScopedDataKindV4) -> String {
  match value {
    ScopedWWItem => "ww-item"
    ScopedWWPredicate => "ww-predicate"
    ScopedWRItem => "wr-item"
    ScopedWRPredicate => "wr-predicate"
    ScopedRWItem => "rw-item"
    ScopedRWPredicate => "rw-predicate"
  }
}

///|
fn scoped_order_name_v4(value : ScopedOrderKindV4) -> String {
  match value {
    ScopedProcess => "process"
    ScopedRealtime => "realtime"
  }
}

///|
fn scoped_name_valid_v4(value : String) -> Bool {
  if value.length() < 1 || value.length() > 256 {
    return false
  }
  try {
    relation_v3_scalar(value)
    true
  } catch {
    _ => false
  }
}

///|
fn scoped_ref_valid_v4(
  reference : ScopedOriginalRefV4,
  expected : String,
) -> Bool {
  if reference.transaction != expected {
    return false
  }
  match reference.observation {
    Object(fields) => {
      if fields.is_empty() {
        return false
      }
      let tx = fields.get("tx")
      let transaction = fields.get("transaction")
      if tx != Some(Json(expected)) && transaction != Some(Json(expected)) {
        return false
      }
      (tx is None || tx == Some(Json(expected))) &&
      (transaction is None || transaction == Some(Json(expected)))
    }
    _ => false
  }
}

///|
fn scoped_order_bad_nodes_v4(
  count : Int32,
  edges : Array[ScopedIndexedEdgeV4],
  kind : ScopedOrderKindV4?,
) -> FixedArray[Bool] {
  let indegree = FixedArray::make(count, 0)
  let adjacency : Array[Array[Int32]] = []
  for _ in 0.. 0
  }
  bad
}

///|
fn scoped_sorted_json_v4(value : Json, out : StringBuilder) -> Unit {
  match value {
    Object(fields) => {
      out.write_string("{")
      let keys = fields.keys().collect()
      keys.sort_by((a, b) => relation_v3_text_compare(a, b))
      for position, key in keys {
        if position > 0 {
          out.write_string(",")
        }
        out.write_string(Json(key).stringify())
        out.write_string(":")
        scoped_sorted_json_v4(fields.at(key), out)
      }
      out.write_string("}")
    }
    Array(items) => {
      out.write_string("[")
      for position, item in items {
        if position > 0 {
          out.write_string(",")
        }
        scoped_sorted_json_v4(item, out)
      }
      out.write_string("]")
    }
    _ => out.write_string(value.stringify())
  }
}

///|
/// The snapshot binds caller-declared graph facts. It does not authenticate a DB.
/// Missing edge provenance is excluded and makes only its required context unknown.
pub fn scoped_dependency_graph_v4(
  nodes : Array[String],
  data_edges : Array[ScopedDataEdgeInputV4],
  order_edges~ : Array[ScopedOrderEdgeInputV4] = [],
  data_complete~ : Bool = true,
  process_complete~ : Bool = false,
  realtime_complete~ : Bool = false,
  gaps~ : Array[Json] = [],
) -> Result[ScopedDependencyGraphV4, String] {
  if nodes.length() > 128 ||
    data_edges.length() + order_edges.length() > 50000 ||
    gaps.length() > 4096 {
    return Err(
      "scoped graph capacity is 128 nodes, 50000 edges, 4096 coverage gaps",
    )
  }
  let node_indices : Map[String, Int32] = Map([])
  for position, node in nodes {
    if !scoped_name_valid_v4(node) || node_indices.contains(node) {
      return Err(
        "node identities must be unique nonempty Unicode scalar strings of at most 256 UTF-16 units",
      )
    }
    node_indices[node] = position
  }
  let edge_ids : Map[String, Bool] = Map([])
  let indexed : Array[ScopedIndexedEdgeV4] = []
  let raw_data : Array[Json] = []
  let raw_order : Array[Json] = []
  let data_gaps = gaps.map(v4_json_copy)
  let process_gaps : Array[Json] = []
  let realtime_gaps : Array[Json] = []
  for edge in data_edges {
    if !scoped_name_valid_v4(edge.id) ||
      edge_ids.contains(edge.id) ||
      !node_indices.contains(edge.from) ||
      !node_indices.contains(edge.to) ||
      edge.from == edge.to {
      return Err(
        "edge IDs must be unique, endpoints must name distinct declared nodes",
      )
    }
    edge_ids[edge.id] = true
    let record : Json = {
      "id": Json(edge.id),
      "from": Json(edge.from),
      "to": Json(edge.to),
      "kind": Json(scoped_data_name_v4(edge.kind)),
      "sources": Json(edge.sources.map(v4_json_copy)),
    }
    raw_data.push(record)
    if edge.sources.is_empty() ||
      edge.sources.any(source => source == Json::null()) {
      data_gaps.push({
        "code": "missing-scoped-data-edge-source",
        "edgeId": Json(edge.id),
      })
      continue
    }
    indexed.push({
      id: edge.id,
      from: node_indices.at(edge.from),
      to: node_indices.at(edge.to),
      data_kind: Some(edge.kind),
      order_kind: None,
      original: record,
    })
  }
  let process_membership : Map[String, String] = Map([])
  let process_conflicts : Map[Int32, Bool] = Map([])
  for edge in order_edges {
    if !scoped_name_valid_v4(edge.id) ||
      edge_ids.contains(edge.id) ||
      !node_indices.contains(edge.from) ||
      !node_indices.contains(edge.to) ||
      edge.from == edge.to {
      return Err(
        "edge IDs must be unique, endpoints must name distinct declared nodes",
      )
    }
    edge_ids[edge.id] = true
    let record : Json = {
      "id": Json(edge.id),
      "from": Json(edge.from),
      "to": Json(edge.to),
      "kind": Json(scoped_order_name_v4(edge.kind)),
      "contextId": Json(edge.context_id),
      "fromOriginal": {
        "transaction": Json(edge.from_source.transaction),
        "observation": v4_json_copy(edge.from_source.observation),
      },
      "toOriginal": {
        "transaction": Json(edge.to_source.transaction),
        "observation": v4_json_copy(edge.to_source.observation),
      },
    }
    raw_order.push(record)
    let valid = scoped_name_valid_v4(edge.context_id) &&
      scoped_ref_valid_v4(edge.from_source, edge.from) &&
      scoped_ref_valid_v4(edge.to_source, edge.to)
    if !valid {
      let target = if edge.kind == ScopedProcess {
        process_gaps
      } else {
        realtime_gaps
      }
      target.push({
        "code": "unbound-scoped-order-source",
        "edgeId": Json(edge.id),
        "original": record,
      })
      continue
    }
    if edge.kind == ScopedProcess {
      for node in [edge.from, edge.to] {
        match process_membership.get(node) {
          Some(previous) if previous != edge.context_id =>
            process_conflicts[node_indices.at(node)] = true
          _ => process_membership[node] = edge.context_id
        }
      }
    }
    indexed.push({
      id: edge.id,
      from: node_indices.at(edge.from),
      to: node_indices.at(edge.to),
      data_kind: None,
      order_kind: Some(edge.kind),
      original: record,
    })
  }
  let bad_process_nodes = scoped_order_bad_nodes_v4(
    nodes.length(),
    indexed,
    Some(ScopedProcess),
  )
  let bad_realtime_nodes = scoped_order_bad_nodes_v4(
    nodes.length(),
    indexed,
    Some(ScopedRealtime),
  )
  let bad_process_ids : Array[Json] = []
  let bad_realtime_ids : Array[Json] = []
  for node in 0.. {
    !(edge.order_kind == Some(ScopedProcess) &&
    (bad_process_nodes[edge.from] || bad_process_nodes[edge.to])) &&
    !(edge.order_kind == Some(ScopedRealtime) &&
    (bad_realtime_nodes[edge.from] || bad_realtime_nodes[edge.to]))
  })
  let adjacency : Array[Array[Int32]] = []
  for _ in nodes {
    adjacency.push([])
  }
  for position, edge in edges {
    adjacency[edge.from].push(position)
  }
  let input_record : Json = {
    "format": "moontxnwitness-declared-scoped-graph/v1",
    "nodes": Json(nodes.copy()),
    "dataEdges": Json(raw_data),
    "orderEdges": Json(raw_order),
    "coverage": {
      "dataComplete": Json(data_complete),
      "processComplete": Json(process_complete),
      "realtimeComplete": Json(realtime_complete),
    },
    "gaps": Json(gaps.map(v4_json_copy)),
  }
  let encoded = StringBuilder()
  scoped_sorted_json_v4(input_record, encoded)
  Ok({
    nodes: nodes.copy(),
    edges,
    adjacency,
    input_sha256: v3_dislock_sha256(encoded.to_string()),
    input_record,
    data_complete: data_complete && data_gaps.is_empty(),
    process_complete: process_complete && process_gaps.is_empty(),
    realtime_complete: realtime_complete && realtime_gaps.is_empty(),
    data_gaps,
    process_gaps,
    realtime_gaps,
  })
}

///|
fn scoped_context_complete_v4(
  graph : ScopedDependencyGraphV4,
  context : ScopedOrderContextV4,
) -> Bool {
  graph.data_complete &&
  (match context {
    ScopedDataOnly => true
    ScopedWithProcess => graph.process_complete
    ScopedWithRealtime => graph.realtime_complete
  })
}

///|
fn scoped_context_gaps_v4(
  graph : ScopedDependencyGraphV4,
  context : ScopedOrderContextV4,
) -> Array[Json] {
  let gaps = graph.data_gaps.copy()
  let specific = match context {
    ScopedDataOnly => []
    ScopedWithProcess => graph.process_gaps
    ScopedWithRealtime => graph.realtime_gaps
  }
  for gap in specific {
    gaps.push(gap)
  }
  if !scoped_context_complete_v4(graph, context) {
    gaps.push({
      "code": "scoped-graph-context-not-proved-complete",
      "context": Json(scoped_context_name_v4(context)),
    })
  }
  gaps
}

///|
pub fn scoped_dependency_graph_v4_json(graph : ScopedDependencyGraphV4) -> Json {
  v4_json_copy({
    "input": graph.input_record,
    "inputSha256": Json(graph.input_sha256),
    "inputEncoding": "sorted-json-object-keys-v1; array order and primitive encodings retained",
    "sourceAssurance": "caller-declared scoped relations with structurally bound original references; no database authentication",
    "effectiveEdgeIds": Json(graph.edges.map(edge => Json(edge.id))),
    "derivedCoverage": {
      "dataComplete": Json(graph.data_complete),
      "processComplete": Json(graph.process_complete),
      "realtimeComplete": Json(graph.realtime_complete),
    },
    "dataGaps": Json(graph.data_gaps),
    "processGaps": Json(graph.process_gaps),
    "realtimeGaps": Json(graph.realtime_gaps),
  })
}