/**
 * @module graph/analyticsFallback
 * @description Pure-TS graph-analytics kernels — the fallback implementations
 * behind `brain.graph.rank` / `brain.graph.communities` when no native
 * {@link import('../plugin.js').GraphAccelerationProvider} is registered. Each
 * operates on a dense integer adjacency (`outAdj[i]` = the target indices of node
 * `i`'s out-edges, multiplicity preserved) so callers can build it once from the
 * UUID graph and run any kernel.
 *
 * These are INTENT-level fallbacks: the public API promises the QUESTION
 * ("which nodes matter most", "which things group together") — these answer it
 * with the standard textbook algorithm (PageRank, connected components,
 * Tarjan SCC). A native provider may answer the same intent with a different
 * algorithm (personalized PageRank, Louvain, etc.); correctness of the INTENT,
 * not the algorithm, is the contract.
 *
 * Correct at small/medium scale (the native provider is the at-scale path). All
 * graph walks are iterative (explicit stacks/queues) so a deep or wide graph
 * never blows the call stack.
 */

/** Adjacency where `outAdj[i]` lists the target node indices of `i`'s out-edges. */
export type DenseAdjacency = ReadonlyArray<ReadonlyArray<number>>

/**
 * @description Label each node with the id of its WEAKLY-connected component
 *   (edges treated as undirected) via union-find with path compression + a single
 *   pass over the adjacency. Two nodes share a label iff one can reach the other
 *   ignoring edge direction.
 * @param outAdj - Dense out-adjacency.
 * @returns `labels[i]` = the component representative of node `i` (labels are
 *   stable per component but NOT necessarily contiguous — group by equality).
 */
export function connectedComponents(outAdj: DenseAdjacency): number[] {
  const n = outAdj.length
  const parent = new Array<number>(n)
  for (let i = 0; i < n; i++) parent[i] = i

  const find = (x: number): number => {
    let root = x
    while (parent[root] !== root) root = parent[root]
    // Path compression: point every node on the chain straight at the root.
    while (parent[x] !== root) {
      const next = parent[x]
      parent[x] = root
      x = next
    }
    return root
  }

  for (let i = 0; i < n; i++) {
    const neighbors = outAdj[i]
    for (let k = 0; k < neighbors.length; k++) {
      const ra = find(i)
      const rb = find(neighbors[k])
      if (ra !== rb) parent[ra] = rb
    }
  }

  const labels = new Array<number>(n)
  for (let i = 0; i < n; i++) labels[i] = find(i)
  return labels
}

/**
 * @description Label each node with its STRONGLY-connected component (directed —
 *   two nodes share a label iff each is reachable from the other following edge
 *   direction) via an iterative Tarjan's algorithm.
 * @param outAdj - Dense out-adjacency.
 * @returns `labels[i]` = the SCC index of node `i` (contiguous `0..count-1`).
 */
export function stronglyConnectedComponents(outAdj: DenseAdjacency): number[] {
  const n = outAdj.length
  const index = new Array<number>(n).fill(-1)
  const low = new Array<number>(n).fill(0)
  const onStack = new Array<boolean>(n).fill(false)
  const comp = new Array<number>(n).fill(-1)
  const sccStack: number[] = []
  let nextIndex = 0
  let compCount = 0

  for (let start = 0; start < n; start++) {
    if (index[start] !== -1) continue
    // Explicit DFS stack of frames; `pi` is the next out-edge to visit for `node`.
    const callStack: Array<{ node: number; pi: number }> = [{ node: start, pi: 0 }]
    while (callStack.length > 0) {
      const frame = callStack[callStack.length - 1]
      const v = frame.node
      if (frame.pi === 0) {
        index[v] = low[v] = nextIndex++
        sccStack.push(v)
        onStack[v] = true
      }
      if (frame.pi < outAdj[v].length) {
        const w = outAdj[v][frame.pi]
        frame.pi++
        if (index[w] === -1) {
          callStack.push({ node: w, pi: 0 })
        } else if (onStack[w]) {
          if (index[w] < low[v]) low[v] = index[w]
        }
      } else {
        // All edges of v explored — if v roots an SCC, pop it off the SCC stack.
        if (low[v] === index[v]) {
          for (;;) {
            const w = sccStack.pop() as number
            onStack[w] = false
            comp[w] = compCount
            if (w === v) break
          }
          compCount++
        }
        callStack.pop()
        if (callStack.length > 0) {
          const parent = callStack[callStack.length - 1].node
          if (low[v] < low[parent]) low[parent] = low[v]
        }
      }
    }
  }

  return comp
}

/** Tuning knobs for {@link pageRank}. */
export interface PageRankOptions {
  /** Teleport/damping factor (default `0.85`). */
  damping?: number
  /** Max power-iterations before stopping (default `100`). */
  maxIterations?: number
  /** L1 convergence threshold on the score vector (default `1e-6`). */
  tolerance?: number
}

/**
 * @description PageRank importance score per node via power-iteration. Dangling
 *   nodes (no out-edges) redistribute their mass uniformly so the score vector
 *   stays a probability distribution (sums to 1). Edge multiplicity is honored
 *   (a node with two edges to the same target sends it twice the share).
 * @param outAdj - Dense out-adjacency.
 * @param options - Damping / iteration / tolerance knobs.
 * @returns `scores[i]` = node `i`'s PageRank (higher = more central).
 */
export function pageRank(outAdj: DenseAdjacency, options?: PageRankOptions): Float64Array {
  const n = outAdj.length
  if (n === 0) return new Float64Array(0)

  const damping = options?.damping ?? 0.85
  const maxIterations = options?.maxIterations ?? 100
  const tolerance = options?.tolerance ?? 1e-6

  const outDegree = new Array<number>(n)
  for (let i = 0; i < n; i++) outDegree[i] = outAdj[i].length

  let rank = new Float64Array(n)
  rank.fill(1 / n)
  const teleport = (1 - damping) / n

  for (let iter = 0; iter < maxIterations; iter++) {
    // Mass stranded on dangling nodes this round, spread uniformly to everyone.
    let danglingMass = 0
    for (let i = 0; i < n; i++) if (outDegree[i] === 0) danglingMass += rank[i]
    const danglingShare = (damping * danglingMass) / n

    const next = new Float64Array(n)
    next.fill(teleport + danglingShare)

    for (let i = 0; i < n; i++) {
      const degree = outDegree[i]
      if (degree === 0) continue
      const share = (damping * rank[i]) / degree
      const neighbors = outAdj[i]
      for (let k = 0; k < neighbors.length; k++) next[neighbors[k]] += share
    }

    let delta = 0
    for (let i = 0; i < n; i++) delta += Math.abs(next[i] - rank[i])
    rank = next
    if (delta < tolerance) break
  }

  return rank
}

/**
 * @description A minimal binary min-heap (priority queue) — used by the weighted
 *   shortest-path (Dijkstra) fallback to pop the lowest-cost frontier node in
 *   O(log n). Stable enough for pathfinding; ties pop in unspecified order.
 */
export class MinHeap<T> {
  private readonly items: Array<{ value: T; priority: number }> = []

  /** Number of queued items. */
  get size(): number {
    return this.items.length
  }

  /**
   * @description Insert `value` with the given `priority` (lower pops first).
   * @param value - The payload.
   * @param priority - Ordering key (ascending).
   */
  push(value: T, priority: number): void {
    const items = this.items
    items.push({ value, priority })
    let i = items.length - 1
    while (i > 0) {
      const parent = (i - 1) >> 1
      if (items[parent].priority <= items[i].priority) break
      const tmp = items[parent]
      items[parent] = items[i]
      items[i] = tmp
      i = parent
    }
  }

  /**
   * @description Remove and return the lowest-priority value.
   * @returns The min value, or `undefined` when empty.
   */
  pop(): T | undefined {
    const items = this.items
    if (items.length === 0) return undefined
    const top = items[0].value
    const last = items.pop() as { value: T; priority: number }
    if (items.length > 0) {
      items[0] = last
      let i = 0
      for (;;) {
        const left = 2 * i + 1
        const right = 2 * i + 2
        let smallest = i
        if (left < items.length && items[left].priority < items[smallest].priority) smallest = left
        if (right < items.length && items[right].priority < items[smallest].priority) smallest = right
        if (smallest === i) break
        const tmp = items[smallest]
        items[smallest] = items[i]
        items[i] = tmp
        i = smallest
      }
    }
    return top
  }
}
