import type { JsonEdge, Node } from '@api-client/graph/graph/types.js'
import type { DataDomain, DataDomainSchema } from './DataDomain.js'
import type { DomainGraphEdge, DataDomainGraph, SerializedGraph } from './types.js'

/**
 * Serializes a DataDomain for version release, including only relevant foreign nodes.
 * @param root The DataDomain to serialize.
 * @param version The version to set in the serialized graph. This mutates the DataDomain.
 * @returns The serialized graph with the version set.
 */
export function createGraphVersion(root: DataDomain, version: string): DataDomainSchema {
  root.info.version = version
  const graph = serializeForVersionRelease(root)
  const result: DataDomainSchema = {
    key: root.key,
    kind: root.kind,
    info: root.info.toJSON(),
    graph,
  }
  if (root.dependencyList.length) {
    // Note: the used dependencies are serialized in the graph.
    // The dependency list is only preserved for linage purposes.
    result.dependencyList = [...root.dependencyList]
  }
  if (root.fields.length) {
    result.fields = [...root.fields]
  }
  return result
}

/**
 * Serializes a DataDomain for version release, including only relevant foreign nodes.
 *
 * @param dataDomain The DataDomain to serialize.
 * @returns A SerializedGraph containing the versioned data.
 */
export function serializeForVersionRelease(dataDomain: DataDomain): SerializedGraph {
  const { graph, key: domainKey } = dataDomain
  const relevantForeignNodes = new Set<string>()

  // 1. Identify relevant foreign nodes: those connected to local entities.
  for (const node of graph.nodes()) {
    const nodeValue = graph.node(node)
    if (!nodeValue) {
      continue
    }
    if (nodeValue.domain.key === domainKey) {
      continue // Skip local nodes
    }

    // Check if the foreign node is connected to a local node (directly or indirectly).
    if (isConnectedToLocal(graph, node, domainKey)) {
      relevantForeignNodes.add(node)
    }
  }

  // 2. Filter nodes and edges based on relevance.
  const nodes = []
  const edges = []

  for (const node of graph.nodes()) {
    const nodeValue = graph.node(node)
    if (!nodeValue) {
      continue
    }

    if (nodeValue.domain.key === domainKey || relevantForeignNodes.has(node)) {
      const parents = [...graph.parents(node)]
      const serializedNode: Node<object> = { v: node, value: nodeValue.toJSON() }
      if (parents.length) {
        serializedNode.parents = parents
      }
      nodes.push(serializedNode)
    }
  }

  for (const edge of graph.edges()) {
    const edgeValue = graph.edge(edge)
    if (!edgeValue) continue

    const sourceNode = graph.node(edge.v)
    const targetNode = graph.node(edge.w)

    if (!sourceNode || !targetNode) continue

    const isSourceRelevant = sourceNode.domain.key === domainKey || relevantForeignNodes.has(edge.v)
    const isTargetRelevant = targetNode.domain.key === domainKey || relevantForeignNodes.has(edge.w)

    if (isSourceRelevant && isTargetRelevant) {
      const serializedEdge: JsonEdge<DomainGraphEdge> = { v: edge.v, w: edge.w, value: structuredClone(edgeValue) }
      if (edge.name) {
        serializedEdge.name = edge.name
      }
      edges.push(serializedEdge)
    }
  }

  // 3. Construct the SerializedGraph.
  const json: SerializedGraph = {
    options: {
      directed: graph.isDirected(),
      multigraph: graph.isMultigraph(),
      compound: graph.isCompound(),
    },
    nodes,
    edges,
  }

  return json
}

/**
 * Checks if a foreign node is connected to a local node (directly or indirectly).
 *
 * This function performs a breadth-first search (BFS) to determine if a foreign node
 * is connected to any local node.
 *
 * @param g The DataDomain graph.
 * @param node The key of the foreign node to check.
 * @param domainKey The key of the local domain.
 * @returns True if the foreign node is connected to a local node.
 */
function isConnectedToLocal(g: DataDomainGraph, node: string, domainKey: string): boolean {
  const visited = new Set<string>()
  const queue: string[] = [node]

  while (queue.length > 0) {
    const current = queue.shift() as string
    if (visited.has(current)) {
      continue
    }
    visited.add(current)

    const value = g.node(current)
    if (!value) {
      continue
    }

    if (value.domain.key === domainKey) {
      // It means the foreign node is connected to a local node.
      return true
    }

    // Explore adjacent nodes (both incoming and outgoing edges).
    for (const edge of g.inEdges(current)) {
      queue.push(edge.v)
    }
    for (const edge of g.outEdges(current)) {
      const label = g.edge(edge)
      let id = edge.w
      if (label?.foreign) {
        id = `${label.domain}:${edge.w}`
      }
      queue.push(id)
    }
  }

  return false // No connection to a local node found.
}
