## Graph Engine — adjacency list storage with graph algorithms
import std/tables
import std/deques
import std/algorithm
import std/math
import std/sets
import std/hashes
import std/streams
import std/locks
import std/sequtils

type
  EdgeId* = distinct uint64
  NodeId* = distinct uint64

  Edge* = object
    id*: EdgeId
    src*: NodeId
    dst*: NodeId
    label*: string
    properties*: Table[string, string]
    weight*: float64

  GraphNode* = object
    id*: NodeId
    label*: string
    properties*: Table[string, string]

  AdjacencyEntry* = object
    edgeId*: EdgeId
    neighbor*: NodeId
    weight*: float64
    label*: string

  Graph* = ref object
    nodes*: Table[NodeId, GraphNode]
    edges*: Table[EdgeId, Edge]
    adjacency*: Table[NodeId, seq[AdjacencyEntry]]  # outgoing
    reverseAdj*: Table[NodeId, seq[AdjacencyEntry]]  # incoming
    nextNodeId: uint64
    nextEdgeId: uint64
    lock*: Lock

proc `==`*(a, b: EdgeId): bool = uint64(a) == uint64(b)
proc `==`*(a, b: NodeId): bool = uint64(a) == uint64(b)
proc hash*(x: EdgeId): Hash = hash(uint64(x))
proc hash*(x: NodeId): Hash = hash(uint64(x))
proc `$`*(x: NodeId): string = $(uint64(x))
proc `$`*(x: EdgeId): string = $(uint64(x))

proc newGraph*(): Graph =
  new(result)
  initLock(result.lock)
  result.nodes = initTable[NodeId, GraphNode]()
  result.edges = initTable[EdgeId, Edge]()
  result.adjacency = initTable[NodeId, seq[AdjacencyEntry]]()
  result.reverseAdj = initTable[NodeId, seq[AdjacencyEntry]]()
  result.nextNodeId = 1
  result.nextEdgeId = 1

proc addNode*(g: Graph, label: string, properties: Table[string, string] = initTable[string, string]()): NodeId =
  acquire(g.lock)
  defer: release(g.lock)
  let id = NodeId(g.nextNodeId)
  inc g.nextNodeId
  g.nodes[id] = GraphNode(id: id, label: label, properties: properties)
  g.adjacency[id] = @[]
  g.reverseAdj[id] = @[]
  return id

proc addNodeWithId*(g: Graph, id: NodeId, label: string,
                     properties: Table[string, string] = initTable[string, string]()) =
  acquire(g.lock)
  defer: release(g.lock)
  if id notin g.nodes:
    g.nodes[id] = GraphNode(id: id, label: label, properties: properties)
    g.adjacency[id] = @[]
    g.reverseAdj[id] = @[]
    if uint64(id) >= g.nextNodeId:
      g.nextNodeId = uint64(id) + 1

proc addEdge*(g: Graph, src, dst: NodeId, label: string = "",
              properties: Table[string, string] = initTable[string, string](),
              weight: float64 = 1.0): EdgeId =
  acquire(g.lock)
  defer: release(g.lock)
  if src notin g.nodes:
    raise newException(KeyError, "Source node does not exist: " & $uint64(src))
  if dst notin g.nodes:
    raise newException(KeyError, "Destination node does not exist: " & $uint64(dst))
  let id = EdgeId(g.nextEdgeId)
  inc g.nextEdgeId
  g.edges[id] = Edge(id: id, src: src, dst: dst, label: label,
                     properties: properties, weight: weight)
  g.adjacency[src].add(AdjacencyEntry(edgeId: id, neighbor: dst, weight: weight, label: label))
  g.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src, weight: weight, label: label))
  return id

proc addEdgeWithId*(g: Graph, src, dst: NodeId, label: string = "",
                     weight: float64 = 1.0) =
  acquire(g.lock)
  defer: release(g.lock)
  if src notin g.nodes:
    raise newException(KeyError, "Source node does not exist: " & $(uint64(src)))
  if dst notin g.nodes:
    raise newException(KeyError, "Destination node does not exist: " & $(uint64(dst)))
  let id = EdgeId(g.nextEdgeId)
  inc g.nextEdgeId
  g.edges[id] = Edge(id: id, src: src, dst: dst, label: label,
                     properties: initTable[string, string](), weight: weight)
  g.adjacency[src].add(AdjacencyEntry(edgeId: id, neighbor: dst, weight: weight, label: label))
  g.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src, weight: weight, label: label))

proc getNode*(g: Graph, id: NodeId): GraphNode =
  acquire(g.lock)
  defer: release(g.lock)
  return g.nodes[id]

proc getEdge*(g: Graph, id: EdgeId): Edge =
  acquire(g.lock)
  defer: release(g.lock)
  return g.edges[id]

proc neighbors*(g: Graph, nodeId: NodeId): seq[NodeId] =
  acquire(g.lock)
  defer: release(g.lock)
  result = @[]
  for entry in g.adjacency.getOrDefault(nodeId, @[]):
    result.add(entry.neighbor)

proc inNeighbors*(g: Graph, nodeId: NodeId): seq[NodeId] =
  acquire(g.lock)
  defer: release(g.lock)
  result = @[]
  for entry in g.reverseAdj.getOrDefault(nodeId, @[]):
    result.add(entry.neighbor)

proc removeNode*(g: Graph, nodeId: NodeId) =
  acquire(g.lock)
  defer: release(g.lock)
  if nodeId notin g.nodes:
    return

  for entry in g.adjacency.getOrDefault(nodeId, @[]):
    g.edges.del(entry.edgeId)
    var newRev: seq[AdjacencyEntry] = @[]
    for rev in g.reverseAdj.getOrDefault(entry.neighbor, @[]):
      if rev.neighbor != nodeId:
        newRev.add(rev)
    g.reverseAdj[entry.neighbor] = newRev

  for entry in g.reverseAdj.getOrDefault(nodeId, @[]):
    g.edges.del(entry.edgeId)
    var newAdj: seq[AdjacencyEntry] = @[]
    for adj in g.adjacency.getOrDefault(entry.neighbor, @[]):
      if adj.neighbor != nodeId:
        newAdj.add(adj)
    g.adjacency[entry.neighbor] = newAdj

  g.nodes.del(nodeId)
  g.adjacency.del(nodeId)
  g.reverseAdj.del(nodeId)

proc bfs*(g: Graph, start: NodeId, maxDepth: int = -1): seq[NodeId] =
  acquire(g.lock)
  defer: release(g.lock)
  result = @[]
  var visited = initHashSet[NodeId]()
  var queue = initDeque[(NodeId, int)]()
  queue.addLast((start, 0))
  visited.incl(start)

  while queue.len > 0:
    let (node, depth) = queue.popFirst()
    result.add(node)
    if maxDepth >= 0 and depth >= maxDepth:
      continue
    for entry in g.adjacency.getOrDefault(node, @[]):
      if entry.neighbor notin visited:
        visited.incl(entry.neighbor)
        queue.addLast((entry.neighbor, depth + 1))

proc dfs*(g: Graph, start: NodeId, maxDepth: int = -1): seq[NodeId] =
  acquire(g.lock)
  defer: release(g.lock)
  result = @[]
  var visited = initHashSet[NodeId]()
  var stack: seq[(NodeId, int)] = @[(start, 0)]

  while stack.len > 0:
    let (node, depth) = stack.pop()
    if node in visited:
      continue
    visited.incl(node)
    result.add(node)
    if maxDepth >= 0 and depth >= maxDepth:
      continue
    for entry in g.adjacency.getOrDefault(node, @[]):
      if entry.neighbor notin visited:
        stack.add((entry.neighbor, depth + 1))

proc shortestPath*(g: Graph, start, target: NodeId): seq[NodeId] =
  acquire(g.lock)
  defer: release(g.lock)
  var visited = initHashSet[NodeId]()
  var parent = initTable[NodeId, NodeId]()
  var queue = initDeque[NodeId]()
  queue.addLast(start)
  visited.incl(start)

  while queue.len > 0:
    let node = queue.popFirst()
    if node == target:
      var path: seq[NodeId] = @[target]
      var current = target
      while current in parent:
        current = parent[current]
        path.add(current)
      path.reverse()
      return path

    for entry in g.adjacency.getOrDefault(node, @[]):
      if entry.neighbor notin visited:
        visited.incl(entry.neighbor)
        parent[entry.neighbor] = node
        queue.addLast(entry.neighbor)

  return @[]

proc dijkstra*(g: Graph, start: NodeId): Table[NodeId, float64] =
  acquire(g.lock)
  defer: release(g.lock)
  result = initTable[NodeId, float64]()
  var visited = initHashSet[NodeId]()

  result[start] = 0.0

  while true:
    var bestNode: NodeId
    var bestDist = Inf
    for nodeId, dist in result:
      if nodeId notin visited and dist < bestDist:
        bestDist = dist
        bestNode = nodeId

    if bestDist == Inf:
      break

    visited.incl(bestNode)

    for entry in g.adjacency.getOrDefault(bestNode, @[]):
      let newDist = bestDist + entry.weight
      if entry.neighbor notin result or newDist < result[entry.neighbor]:
        result[entry.neighbor] = newDist

proc pageRank*(g: Graph, iterations: int = 20, dampingFactor: float64 = 0.85): Table[NodeId, float64] =
  acquire(g.lock)
  defer: release(g.lock)
  result = initTable[NodeId, float64]()
  let n = g.nodes.len
  if n == 0:
    return

  let initialRank = 1.0 / float64(n)
  for nodeId in g.nodes.keys:
    result[nodeId] = initialRank

  for iter in 0..<iterations:
    var newRanks = initTable[NodeId, float64]()
    var danglingSum: float64 = 0

    for nodeId in g.nodes.keys:
      let outDegree = g.adjacency.getOrDefault(nodeId, @[]).len
      if outDegree == 0:
        danglingSum += result[nodeId]

    for nodeId in g.nodes.keys:
      var rank = (1.0 - dampingFactor) / float64(n)
      rank += dampingFactor * danglingSum / float64(n)

      for entry in g.reverseAdj.getOrDefault(nodeId, @[]):
        let srcOutDegree = g.adjacency.getOrDefault(entry.neighbor, @[]).len
        if srcOutDegree > 0:
          rank += dampingFactor * result[entry.neighbor] / float64(srcOutDegree)

      newRanks[nodeId] = rank

    result = newRanks

proc nodeCount*(g: Graph): int =
  acquire(g.lock)
  defer: release(g.lock)
  return g.nodes.len

proc edgeCount*(g: Graph): int =
  acquire(g.lock)
  defer: release(g.lock)
  return g.edges.len

# ---------------------------------------------------------------------------
# Persistence — binary save/load
# ---------------------------------------------------------------------------

const
  GraphFileMagic = "BGRF"
  GraphFileVersion = 1'u32

proc writeString(s: Stream, str: string) =
  s.write(uint32(str.len))
  if str.len > 0:
    s.writeData(str[0].unsafeAddr, str.len)

proc readString(s: Stream): string =
  let len = s.readUint32()
  if len > 0:
    result = newString(int(len))
    discard s.readData(result[0].addr, int(len))
  else:
    result = ""

proc saveToFile*(g: Graph, path: string) =
  acquire(g.lock)
  defer: release(g.lock)
  let s = newFileStream(path, fmWrite)
  if s.isNil:
    raise newException(IOError, "Cannot open graph file for writing: " & path)

  s.write(GraphFileMagic)
  s.write(GraphFileVersion)
  s.write(uint32(g.nodes.len))
  s.write(uint32(g.edges.len))
  s.write(g.nextNodeId)
  s.write(g.nextEdgeId)

  for nodeId, node in g.nodes:
    s.write(uint64(nodeId))
    s.writeString(node.label)
    s.write(uint32(node.properties.len))
    for key, val in node.properties:
      s.writeString(key)
      s.writeString(val)

  for edgeId, edge in g.edges:
    s.write(uint64(edgeId))
    s.write(uint64(edge.src))
    s.write(uint64(edge.dst))
    s.writeString(edge.label)
    s.write(edge.weight)
    s.write(uint32(edge.properties.len))
    for key, val in edge.properties:
      s.writeString(key)
      s.writeString(val)

  s.close()

proc loadFromFile*(path: string): Graph =
  let s = newFileStream(path, fmRead)
  if s.isNil:
    raise newException(IOError, "Cannot open graph file for reading: " & path)

  let magic = s.readStr(4)
  if magic != GraphFileMagic:
    raise newException(ValueError, "Invalid graph file magic bytes")

  let version = s.readUint32()
  if version != GraphFileVersion:
    raise newException(ValueError, "Unsupported graph file version: " & $version)

  let nodeCount = int(s.readUint32())
  let edgeCount = int(s.readUint32())
  let nextNodeId = s.readUint64()
  let nextEdgeId = s.readUint64()

  result = Graph(
    nodes: initTable[NodeId, GraphNode](),
    edges: initTable[EdgeId, Edge](),
    adjacency: initTable[NodeId, seq[AdjacencyEntry]](),
    reverseAdj: initTable[NodeId, seq[AdjacencyEntry]](),
    nextNodeId: nextNodeId,
    nextEdgeId: nextEdgeId,
    lock: Lock(),
  )
  initLock(result.lock)
  acquire(result.lock)

  for i in 0 ..< nodeCount:
    let id = NodeId(s.readUint64())
    let label = s.readString()
    let propCount = int(s.readUint32())
    var props = initTable[string, string]()
    for j in 0 ..< propCount:
      let key = s.readString()
      let val = s.readString()
      props[key] = val
    result.nodes[id] = GraphNode(id: id, label: label, properties: props)
    result.adjacency[id] = @[]
    result.reverseAdj[id] = @[]

  for i in 0 ..< edgeCount:
    let id = EdgeId(s.readUint64())
    let src = NodeId(s.readUint64())
    let dst = NodeId(s.readUint64())
    let label = s.readString()
    let weight = s.readFloat64()
    let propCount = int(s.readUint32())
    var props = initTable[string, string]()
    for j in 0 ..< propCount:
      let key = s.readString()
      let val = s.readString()
      props[key] = val
    result.edges[id] = Edge(id: id, src: src, dst: dst, label: label,
                            properties: props, weight: weight)
    result.adjacency[src].add(AdjacencyEntry(edgeId: id, neighbor: dst,
                                              weight: weight, label: label))
    result.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src,
                                               weight: weight, label: label))

  release(result.lock)
  s.close()

# ---------------------------------------------------------------------------
# Node similarity — Jaccard / Adamic-Adar
# ---------------------------------------------------------------------------

type
  SimilarityMetric* = enum
    smJaccard = "jaccard"
    smAdamicAdar = "adamic_adar"

proc jaccardSimilarityUnlocked(g: Graph, a, b: NodeId): float64 =
  var aNeighbors = initHashSet[NodeId]()
  var bNeighbors = initHashSet[NodeId]()
  for entry in g.adjacency.getOrDefault(a, @[]):
    aNeighbors.incl(entry.neighbor)
  for entry in g.adjacency.getOrDefault(b, @[]):
    bNeighbors.incl(entry.neighbor)
  for entry in g.reverseAdj.getOrDefault(a, @[]):
    aNeighbors.incl(entry.neighbor)
  for entry in g.reverseAdj.getOrDefault(b, @[]):
    bNeighbors.incl(entry.neighbor)

  if aNeighbors.len == 0 and bNeighbors.len == 0:
    return 0.0

  var intersection = 0
  for n in aNeighbors:
    if n in bNeighbors:
      inc intersection
  let union = aNeighbors.len + bNeighbors.len - intersection

  if union == 0:
    return 0.0
  return float64(intersection) / float64(union)

proc jaccardSimilarity*(g: Graph, a, b: NodeId): float64 =
  acquire(g.lock)
  defer: release(g.lock)
  return jaccardSimilarityUnlocked(g, a, b)

proc adamicAdarSimilarityUnlocked(g: Graph, a, b: NodeId): float64 =
  var aNeighbors = initHashSet[NodeId]()
  var bNeighbors = initHashSet[NodeId]()
  for entry in g.adjacency.getOrDefault(a, @[]):
    aNeighbors.incl(entry.neighbor)
  for entry in g.adjacency.getOrDefault(b, @[]):
    bNeighbors.incl(entry.neighbor)

  var sum: float64 = 0
  for n in aNeighbors:
    if n in bNeighbors:
      let degree = g.adjacency.getOrDefault(n, @[]).len.float64
      if degree > 0:
        sum += 1.0 / ln(degree)
  return sum

proc adamicAdarSimilarity*(g: Graph, a, b: NodeId): float64 =
  acquire(g.lock)
  defer: release(g.lock)
  return adamicAdarSimilarityUnlocked(g, a, b)

proc similarityNodes*(g: Graph, metric: SimilarityMetric = smJaccard): seq[(NodeId, NodeId, float64)] =
  acquire(g.lock)
  defer: release(g.lock)
  var nodeList = g.nodes.keys.toSeq
  result = @[]
  for i in 0 ..< nodeList.len:
    for j in i + 1 ..< nodeList.len:
      let a = nodeList[i]
      let b = nodeList[j]
      let sim = case metric
        of smJaccard: jaccardSimilarityUnlocked(g, a, b)
        of smAdamicAdar: adamicAdarSimilarityUnlocked(g, a, b)
      if sim > 0:
        result.add((a, b, sim))

# ---------------------------------------------------------------------------
# Node2Vec — simplified random-walk based graph embeddings
# ---------------------------------------------------------------------------

proc node2vec*(g: Graph, dimensions: int = 64, walkLength: int = 10,
               numWalks: int = 5): Table[NodeId, seq[float32]] =
  ## Generate low-dimensional embeddings for graph nodes via random walks.
  ## Simplified Node2Vec-style approach: random walks + SVD-like factorization.
  result = initTable[NodeId, seq[float32]]()
  if g.nodes.len == 0:
    return

  var nodeList = g.nodes.keys.toSeq
  var nodeIndex = initTable[NodeId, int]()
  for i, nid in nodeList:
    nodeIndex[nid] = i

  var cooccurrence = newSeq[seq[int]](nodeList.len)
  for i in 0 ..< nodeList.len:
    cooccurrence[i] = newSeq[int](nodeList.len)

  # Random walks from each node
  for nid in nodeList:
    for w in 0 ..< numWalks:
      var current = nid
      for step in 0 ..< walkLength:
        let neighbors = g.adjacency.getOrDefault(current, @[])
        if neighbors.len == 0:
          break
        let nbr = neighbors[(step * 17 + w * 31) mod neighbors.len]
        cooccurrence[nodeIndex[current]][nodeIndex[nbr.neighbor]] += 1
        cooccurrence[nodeIndex[nbr.neighbor]][nodeIndex[current]] += 1
        current = nbr.neighbor

  # Simple projection: use co-occurrence counts as embedding features
  for i, nid in nodeList:
    var emb = newSeq[float32](dimensions)
    var total = 0
    for j in 0 ..< nodeList.len:
      if i != j:
        total += cooccurrence[i][j]
    for d in 0 ..< dimensions:
      if total > 0:
        emb[d] = float32(cooccurrence[i][min(d + (i * 7), nodeList.len - 1)]) / float32(max(total, 1))
      else:
        emb[d] = float32(0.01)
    result[nid] = emb
