///|
priv struct V4CategoryGraph {
  base : ClassificationModelV3Graph
  inferences : Json
}

///|
fn v4_empty_category_graph(
  history : HistoryV4,
  reason : String,
) -> V4CategoryGraph {
  let normalized = match history.payload {
    RegisterSnapshot(_, Some(n)) => n
    ListSnapshot(parsed) => parsed.normalized
    RelationSnapshot(parsed) => parsed.normalized
    _ => Json::object(Map([]))
  }
  {
    base: {
      normalized,
      transactions: [],
      edges: [],
      sources: Map([]),
      validity_gaps: [],
      history: None,
      list_parsed: None,
      complete: false,
      reason,
    },
    inferences: Json::null(),
  }
}

///|
fn v4_register_direct_gap(
  gaps : Array[Json],
  keyed : Map[String, Map[String, Bool]],
  unkeyed : Map[String, Bool],
  code : String,
  edge : Edge,
) -> Unit {
  match edge.key {
    Some(key) => {
      let seen = keyed.get_or_default(code, Map([]))
      if seen.contains(key) {
        return
      }
      seen[key] = true
      keyed[code] = seen
    }
    None => {
      if unkeyed.contains(code) {
        return
      }
      unkeyed[code] = true
    }
  }
  gaps.push({
    "code": Json(code),
    "severity": "uncertainty",
    "message": "A legacy serialization constraint lacks the resolved version order or source metadata required for a direct category RW edge",
    "transactions": Json([edge.from, edge.to]),
    "key": match edge.key {
      Some(key) => Json(key)
      None => Json::null()
    },
    "ops": Json(([] : Array[Json])),
    "edge": edge_json(edge),
  })
}

///|
fn v4_register_rw_metadata(
  by_id : Map[String, Transaction],
  edge : Edge,
) -> Bool {
  match
    (edge.key, edge.from_version, edge.to_version, edge.from_op, edge.to_op) {
    (Some(key), Some(first), Some(last), Some(from_op), Some(to_op)) =>
      match (by_id.get(edge.from), by_id.get(edge.to)) {
        (Some(source), Some(target)) if source.status == Committed &&
          target.status == Committed &&
          from_op >= 0 &&
          from_op < source.ops.length() &&
          to_op >= 0 &&
          to_op < target.ops.length() => {
          let read = source.ops[from_op]
          let write = target.ops[to_op]
          read.kind == Read &&
          read.key == key &&
          read.version == first &&
          write.kind == Write &&
          write.key == key &&
          write.version == last
        }
        _ => false
      }
    _ => false
  }
}

///|
/// The legacy analyzer also emits transitive serialization constraints. The
/// category adapter retains RW only to the resolved immediate installed version.
/// An own successor was removed by add_edge and must never be skipped here.
fn v4_register_direct_category_edges(
  history : History,
  report : Report,
) -> (Array[Edge], Array[Json]) {
  let ranks : Map[String, Map[String, Int32]] = Map([])
  for key, order in report.inferred_orders {
    let positions : Map[String, Int32] = Map([])
    for position, version in order {
      positions[version] = position
    }
    ranks[key] = positions
  }
  let by_id : Map[String, Transaction] = Map([])
  for transaction in history.transactions {
    by_id[transaction.id] = transaction
  }
  let edges : Array[Edge] = []
  let gaps : Array[Json] = []
  let keyed : Map[String, Map[String, Bool]] = Map([])
  let unkeyed : Map[String, Bool] = Map([])
  for edge in report.edges {
    if edge.kind != RW {
      edges.push(edge)
      continue
    }
    if !v4_register_rw_metadata(by_id, edge) {
      v4_register_direct_gap(
        gaps, keyed, unkeyed, "register_direct_rw_source_unresolved", edge,
      )
      continue
    }
    let key = edge.key.unwrap()
    match ranks.get(key) {
      None =>
        v4_register_direct_gap(
          gaps, keyed, unkeyed, "register_direct_rw_order_unresolved", edge,
        )
      Some(positions) => {
        let first = positions.get_or_default(edge.from_version.unwrap(), -1)
        let last = positions.get_or_default(edge.to_version.unwrap(), -1)
        if first < 0 || last < 0 || last <= first {
          v4_register_direct_gap(
            gaps, keyed, unkeyed, "register_direct_rw_version_unresolved", edge,
          )
        } else if last == first + 1 {
          edges.push(edge)
        }
      }
    }
  }
  (edges, gaps)
}

///|
fn v4_category_graph(
  history : HistoryV4,
  list_graph : String,
  max_list_graph_checks : Int32,
) -> V4CategoryGraph raise {
  if list_graph == "effective-v1" {
    return match history.payload {
      ListSnapshot(parsed) => {
        let result = list_graph_effective_v4(
          parsed,
          max_checks=max_list_graph_checks,
        )
        {
          base: result.base,
          inferences: {
            "listGraph": list_graph_effective_v4_summary_json(result),
          },
        }
      }
      _ =>
        v4_empty_category_graph(
          history, "effective-list-graph-requires-append-list-v1",
        )
    }
  }
  match history.payload {
    RegisterSnapshot(Some(input), normalized) => {
      let raw_operations = match normalized {
        Some(Object(fields)) =>
          match fields.get("observations") {
            Some(Array(events)) =>
              events
              .filter(event => {
                let kind = object_fields_unchecked_v3(event).get("kind")
                kind == Some(Json("read")) || kind == Some(Json("write"))
              })
              .length()
            _ => count_ops(input)
          }
        _ => count_ops(input)
      }
      if input.transactions.length() > 128 || raw_operations > 4096 {
        return v4_empty_category_graph(
          history, "register-category-capacity-128-4096",
        )
      }
      // Real-time edges have their own categories; never mix them into G0/G1c/G2.
      let data_history : History = { ..input, model: Serializable, }
      let report = analyze_internal(data_history, false, complete_search=false)
      let (direct_edges, direct_gaps) = v4_register_direct_category_edges(
        data_history, report,
      )
      let validity_gaps = report.findings
        .filter(f => f.severity == "error" || f.severity == "uncertainty")
        .map(finding_json)
      for gap in direct_gaps {
        validity_gaps.push(gap)
      }
      let complete = !report.graph.edge_budget_exceeded &&
        validity_gaps.is_empty()
      {
        base: {
          normalized: normalized.unwrap_or(Json::object(Map([]))),
          transactions: data_history.transactions,
          edges: direct_edges,
          sources: Map([]),
          validity_gaps,
          history: Some(data_history),
          list_parsed: None,
          complete,
          reason: if complete {
            "complete"
          } else {
            "required-version-order-source-or-graph-incomplete"
          },
        },
        inferences: {
          "versionOrders": Json(report.inferred_orders),
          "versionOrderSources": Json(report.order_sources),
        },
      }
    }
    RegisterSnapshot(None, _) =>
      v4_empty_category_graph(history, "capture-coverage-incomplete")
    ListSnapshot(parsed) =>
      {
        // The source remains the original capture; no synthetic events are made.
        base: classification_models_v3_list(
          history.source_text,
          parsed.normalized,
        ),
        inferences: { "versionOrderCompletion": false },
      }
    RelationSnapshot(_) =>
      v4_empty_category_graph(
        history, "relation-category-classification-not-supported",
      )
  }
}

///|
fn v4_register_category_witness(
  graph : ClassificationModelV3Graph,
  cycle : Array[Edge],
) -> Json {
  let history = graph.history.unwrap()
  let by_id : Map[String, Transaction] = Map([])
  for tx in history.transactions {
    by_id[tx.id] = tx
  }
  let proof = cycle.map(edge => {
    let from_seq = match edge.from_op {
      Some(index) =>
        classification_v3_observation_sources(
          graph.normalized,
          edge.from,
          by_id.at(edge.from).ops[index],
        )
      None => []
    }
    let to_seq = match edge.to_op {
      Some(index) =>
        classification_v3_observation_sources(
          graph.normalized,
          edge.to,
          by_id.at(edge.to).ops[index],
        )
      None => []
    }
    let order = match edge.key {
      Some(key) =>
        match history.version_orders.get(key) {
          Some(order) => Json(order)
          None => Json::null()
        }
      None => Json::null()
    }
    (
      {
        "edge": edge_json(edge),
        "fromObservationSeq": Json(from_seq),
        "toObservationSeq": Json(to_seq),
        "inputVersionOrder": order,
      } : Json)
  })
  {
    "kind": "source-bound-register-category-cycle",
    "specificCategory": Json(
      if cycle.all(e => e.kind == WW) {
        "G0"
      } else if cycle.any(e => e.kind == RW) {
        "G2-item"
      } else {
        "G1c"
      },
    ),
    "edges": Json(proof),
    "minimality": "simple cycle for the requested category; no shortest-cycle claim",
    "sourceScope": "original capture observation identities or caller-declared transaction operation indices; input assertions do not authenticate a database",
  }
}

///|
fn v4_classification_effects(history : HistoryV4) -> Json {
  let normalized = match history.payload {
    RegisterSnapshot(Some(h), None) => return v4_declared_effects(h)
    RegisterSnapshot(_, Some(n)) => n
    ListSnapshot(parsed) => parsed.normalized
    RelationSnapshot(parsed) => parsed.normalized
    _ => return Json::null()
  }
  {
    "basis": "captured-write-effects",
    "entries": object_fields_unchecked_v3(normalized).get_or_default(
      "effects",
      Json(([] : Array[Json])),
    ),
  }
}

///|
fn v4_classification_record(
  history : HistoryV4,
  status : String,
  rules : Json,
  implied : Array[String],
  graph : V4CategoryGraph,
  categories : Array[String],
  max_states : Int32,
  max_edge_checks : Int32,
  list_graph : String,
  max_list_graph_checks : Int32,
) -> ClassificationV4 {
  let record : Json = {
    "format": "moontxnwitness-classification/v4",
    "schemaVersion": 4,
    "semanticContract": "moontxnwitness-category-classification-v4-1",
    "algorithm": Json(
      if categories.contains("G-single") {
        "v4-source-bound-category-g-single-1"
      } else {
        "v4-source-bound-category-bfs-1"
      },
    ),
    "dataModel": Json(v4_model_name(history.data_model)),
    "observations": {
      "sourceFormat": Json(history.source_format),
      "authenticityVerified": false,
      "document": history.source_document,
    },
    "writeEffects": v4_classification_effects(history),
    "inferences": graph.inferences,
    "judgment": {
      "status": Json(status),
      "acceptsIsolationModels": false,
      "scope": Json(
        if categories.contains("G-single") {
          "requested point-register-v1/append-list-v1 item-dependency categories only; G-single has exactly one item RW edge; checked-compatible does not accept a complete isolation model or certify a database"
        } else {
          "requested dependency-cycle categories only; checked-compatible does not accept a complete isolation model or certify a database"
        },
      ),
      "dependencyGraphComplete": Json(graph.base.complete),
      "dependencies": Json(graph.base.edges.map(edge_json)),
      "sourceOrOrderGaps": Json(graph.base.validity_gaps),
      "rules": rules,
      "impliedAnomalies": Json(implied),
    },
    "execution": {
      "counterContract": Json(
        if categories.contains("G-single") {
          "v4-category-g-single-seed-bfs-counters-1"
        } else {
          "v4-category-seed-bfs-counters-1"
        },
      ),
      "categories": Json(categories.copy()),
      "versionOrderCompletion": false,
      "limits": {
        "registerTransactions": 128,
        "registerOperations": 4096,
        "listCommittedTransactions": 6,
        "listOperations": 128,
        "edges": 50000,
        "statesPerCategory": Json(max_states),
        "edgeChecksPerCategory": Json(max_edge_checks),
      },
    },
  }
  if list_graph == "effective-v1" {
    let root = object_fields_unchecked_v3(record)
    root["algorithm"] = Json("v4-source-bound-category-list-effective-1")
    let judgment = object_fields_unchecked_v3(root.at("judgment"))
    judgment["scope"] = Json(
      "requested append-list-v1 effective item-dependency categories only; source-incompatible keys are isolated; checked-compatible does not accept a complete isolation model or certify a database",
    )
    let execution = object_fields_unchecked_v3(root.at("execution"))
    execution["counterContract"] = Json("v4-list-effective-category-counters-1")
    execution["listGraph"] = Json(list_graph)
    match graph.inferences {
      Object(inferences) =>
        match inferences.get("listGraph") {
          Some(Object(summary)) =>
            execution["listGraphConstruction"] = summary.at("execution")
          _ => ()
        }
      _ => ()
    }
    let limits = object_fields_unchecked_v3(execution.at("limits"))
    let _ = limits.remove("listCommittedTransactions")
    limits["listTransactions"] = Json(128)
    limits["listOperations"] = Json(4096)
    limits["listGraphChecks"] = Json(max_list_graph_checks)
  }
  { status, record, }
}

///|
/// Each requested category has its own search and budget over one shared graph.
pub fn classify_v4(
  history : HistoryV4,
  categories~ : Array[String] = ["G0", "G1c", "G2-item"],
  max_states~ : Int32 = 100000,
  max_edge_checks~ : Int32 = 1000000,
  list_graph~ : String = "legacy",
  max_list_graph_checks~ : Int32 = 1000000,
) -> ClassificationV4 {
  let categories = categories.copy()
  let seen : Map[String, Bool] = Map([])
  let valid = !categories.is_empty() &&
    categories.all(category => {
      if !["G0", "G1c", "G2-item", "G-single"].contains(category) ||
        seen.contains(category) {
        false
      } else {
        seen[category] = true
        true
      }
    }) &&
    max_states >= 0 &&
    max_states <= 100000 &&
    max_edge_checks >= 0 &&
    max_edge_checks <= 1000000 &&
    ["legacy", "effective-v1"].contains(list_graph) &&
    max_list_graph_checks >= 0 &&
    max_list_graph_checks <= 1000000 &&
    (list_graph == "effective-v1" || max_list_graph_checks == 1000000)
  if !valid {
    return v4_classification_record(
      history,
      "invalid",
      Json::object(Map([])),
      [],
      v4_empty_category_graph(history, "invalid-options"),
      categories,
      max_states,
      max_edge_checks,
      list_graph,
      max_list_graph_checks,
    )
  }
  let graph = v4_category_graph(history, list_graph, max_list_graph_checks) catch {
    _ =>
      return v4_classification_record(
        history,
        "invalid",
        Json::object(Map([])),
        [],
        v4_empty_category_graph(history, "invalid-graph-state"),
        categories,
        max_states,
        max_edge_checks,
        list_graph,
        max_list_graph_checks,
      )
  }
  let index = adjacency(graph.base.transactions, graph.base.edges)
  let rules : Map[String, Json] = Map([])
  let implied : Array[String] = []
  let mut status = "checked-compatible"
  for category in categories {
    let budget : ClassificationV3Budget = {
      max_states,
      max_edge_checks,
      states: 0,
      edge_checks: 0,
      limited: false,
      reason: "complete",
    }
    let cycle = if category == "G-single" {
      g_single_cycle_v4(
        graph.base.transactions.length(),
        graph.base.edges,
        index,
        budget,
      )
    } else {
      classification_v3_cycle(
        graph.base.transactions,
        graph.base.edges,
        index,
        category,
        budget,
      )
    }
    let rule_status = if history.data_model == RelationSetV4 ||
      (list_graph == "effective-v1" && history.data_model != AppendListV4) {
      "unsupported"
    } else if !cycle.is_empty() {
      "refuted"
    } else if budget.limited || !graph.base.complete {
      "unknown"
    } else {
      "checked-compatible"
    }
    let reason = if !cycle.is_empty() {
      "source-bound-cycle"
    } else if budget.limited {
      budget.reason
    } else {
      graph.base.reason
    }
    if rule_status == "refuted" {
      status = "refuted"
      let specific = if cycle.all(edge => edge.kind == WW) {
        "G0"
      } else if cycle.any(edge => edge.kind == RW) {
        "G2-item"
      } else {
        "G1c"
      }
      let implications = if category == "G-single" {
        [
          "G-single-item", "G-single", "G-nonadjacent-item", "G-nonadjacent", "G2-item",
          "G2",
        ]
      } else {
        classification_v3_implications(specific)
      }
      for label in implications {
        if !implied.contains(label) {
          implied.push(label)
        }
      }
    } else if status != "refuted" {
      if rule_status == "unsupported" {
        status = "unsupported"
      } else if rule_status == "unknown" {
        status = "inconclusive"
      }
    }
    rules[category] = {
      "status": Json(rule_status),
      "reason": Json(reason),
      "states": Json(budget.states),
      "edgeChecks": Json(budget.edge_checks),
      "evidence": if cycle.is_empty() {
        Json::null()
      } else {
        let witness = if history.data_model == RegisterV4 {
          v4_register_category_witness(graph.base, cycle)
        } else {
          classification_models_v3_witness(graph.base, cycle)
        }
        if list_graph == "effective-v1" {
          let fields = object_fields_unchecked_v3(witness)
          fields["kind"] = Json(
            "source-checked-effective-list-dependency-cycle",
          )
          fields["sourceScope"] = Json(
            "raw delivered reads with effective own suffix at each program position, final committed append groups, and validated external item order; not SQL authenticity",
          )
        }
        if category == "G-single" {
          g_single_category_witness_v4(witness, cycle)
        } else {
          witness
        }
      },
    }
  }
  v4_classification_record(
    history,
    status,
    Json::object(rules),
    implied,
    graph,
    categories,
    max_states,
    max_edge_checks,
    list_graph,
    max_list_graph_checks,
  )
}

///|
pub fn classification_v4_json(result : ClassificationV4) -> Json {
  v4_json_copy(result.record)
}

///|
pub fn render_classification_v4(result : ClassificationV4) -> String {
  result.record.stringify(indent=2)
}

///|
pub fn verify_classification_v4(
  history : HistoryV4,
  supplied : Json,
  categories~ : Array[String] = ["G0", "G1c", "G2-item"],
  max_states~ : Int32 = 100000,
  max_edge_checks~ : Int32 = 1000000,
  list_graph~ : String = "legacy",
  max_list_graph_checks~ : Int32 = 1000000,
) -> Bool {
  supplied ==
  classify_v4(
    history,
    categories~,
    max_states~,
    max_edge_checks~,
    list_graph~,
    max_list_graph_checks~,
  ).record
}