/**
 * The grid as a graph (§5).
 *
 * Everything upstream of this file treats an asset as an isolated unit with its own
 * loading and its own risk. That is enough to say "T-108 is in trouble" and not enough
 * to say what happens *next*, because what happens next is decided by where T-108's load
 * goes when it stops carrying it. This module gives the topology an explicit shape:
 * nodes that carry power and edges that move it.
 *
 * The construction is deterministic and derived from the asset register itself — site
 * membership, region, voltage and capacity — rather than from a hand-drawn map, so the
 * graph cannot drift away from the assets it describes. `buildGridGraph` is pure; the
 * persisted `grid_nodes` / `grid_edges` tables are a snapshot of its output.
 */

import { round, clamp } from './rng'

/** The asset facts the topology needs. A subset of the asset register, by design. */
export interface GraphAsset {
  code: string
  name: string
  nameAr: string
  assetTypeKey: string
  regionCode: string
  siteCode: string
  lat: number
  lon: number
  capacityMw: number
  voltageKv: number
  customersServed: number
}

export type EdgeKind = 'supply' | 'transmission' | 'distribution' | 'service'

export interface GraphNode extends GraphAsset {
  /** Share of national customers that depend on this node, 0–1. */
  centrality: number
  isSupply: boolean
  isLoad: boolean
  /** Customers reachable downstream, including the node's own. */
  downstreamCustomers: number
  downstreamMw: number
}

export interface GraphEdge {
  code: string
  from: string
  to: string
  kind: EdgeKind
  capacityMw: number
  lengthKm: number
  impedance: number
  /** Fraction of the upstream node's outgoing flow that normally travels this edge. */
  shareOfFlow: number
  /** True when losing this edge alone does not disconnect the downstream node. */
  isRedundant: boolean
}

export interface GridGraph {
  nodes: GraphNode[]
  edges: GraphEdge[]
  byCode: Map<string, GraphNode>
  out: Map<string, GraphEdge[]>
  in: Map<string, GraphEdge[]>
  totalCustomers: number
}

const SUPPLY_KINDS = new Set(['power_plant', 'solar_farm', 'wind_farm', 'battery_storage'])
const LOAD_KINDS = new Set(['industrial_load', 'ev_hub'])

/** Great-circle distance, good enough at national scale for a line length. */
function distanceKm(aLat: number, aLon: number, bLat: number, bLon: number): number {
  const toRad = Math.PI / 180
  const meanLat = ((aLat + bLat) / 2) * toRad
  const dx = (bLon - aLon) * toRad * Math.cos(meanLat) * 6371
  const dy = (bLat - aLat) * toRad * 6371
  return Math.sqrt(dx * dx + dy * dy)
}

/** `SS-RYD-03` → 3. Site ordinals order the regional ring. */
function siteOrdinal(siteCode: string): number {
  const tail = siteCode.split('-').pop() ?? '0'
  const value = Number.parseInt(tail, 10)
  return Number.isFinite(value) ? value : 0
}

function makeEdge(
  from: GraphAsset,
  to: GraphAsset,
  kind: EdgeKind,
  capacityMw: number,
  isRedundant: boolean,
): GraphEdge {
  const lengthKm = round(distanceKm(from.lat, from.lon, to.lat, to.lon), 1)
  return {
    code: `${from.code}>${to.code}`,
    from: from.code,
    to: to.code,
    kind,
    capacityMw: round(capacityMw, 1),
    lengthKm,
    // Series impedance rises with length and falls with voltage squared, which is what
    // makes a short high-voltage path the one load actually takes when it is re-routed.
    impedance: round((0.35 + lengthKm * 0.012) / Math.max(1, (to.voltageKv / 132) ** 2), 4),
    shareOfFlow: 0,
    isRedundant,
  }
}

/**
 * Build the national graph.
 *
 * Layers, from generation to meter:
 *   supply → substation → (line → substation)* → transformer → industrial / EV load
 *
 * A transmission line is a node rather than a bare edge because operators reason about
 * lines as assets that overload, trip and appear in a cascade timeline by name.
 */
export function buildGridGraph(assets: GraphAsset[]): GridGraph {
  const sorted = [...assets].sort((a, b) => a.code.localeCompare(b.code))
  const edges: GraphEdge[] = []

  const byRegion = new Map<string, GraphAsset[]>()
  for (const asset of sorted) {
    const bucket = byRegion.get(asset.regionCode)
    if (bucket) bucket.push(asset)
    else byRegion.set(asset.regionCode, [asset])
  }

  for (const [, regionAssets] of byRegion) {
    const substations = regionAssets
      .filter((a) => a.assetTypeKey === 'substation')
      .sort((a, b) => siteOrdinal(a.siteCode) - siteOrdinal(b.siteCode))
    if (substations.length === 0) continue

    const bySite = new Map<string, GraphAsset[]>()
    for (const asset of regionAssets) {
      const bucket = bySite.get(asset.siteCode)
      if (bucket) bucket.push(asset)
      else bySite.set(asset.siteCode, [asset])
    }
    const substationBySite = new Map(substations.map((s) => [s.siteCode, s]))

    // ── Generation feeds the ring ──────────────────────────────────────────
    // Plants and farms sit on their own sites, so they are attached round-robin to the
    // regional substations in code order: deterministic, and it spreads generation
    // across the ring the way a real interconnection would.
    const supply = regionAssets.filter((a) => SUPPLY_KINDS.has(a.assetTypeKey))
    supply.forEach((plant, index) => {
      const target = substations[index % substations.length]
      edges.push(makeEdge(plant, target, 'supply', plant.capacityMw, substations.length > 1))
    })

    // ── Transmission ring ──────────────────────────────────────────────────
    // Each line asset is sited at the substation it leaves from and lands on the next
    // one in the ring; with three or more substations the ring closes, which is what
    // makes any single line redundant.
    const lines = regionAssets.filter((a) => a.assetTypeKey === 'transmission_line')
    const closesRing = substations.length >= 3
    for (const line of lines) {
      const fromSub = substationBySite.get(line.siteCode)
      if (!fromSub) continue
      const fromIndex = substations.indexOf(fromSub)
      const toSub = substations[(fromIndex + 1) % substations.length]
      if (!toSub || toSub.code === fromSub.code) continue
      edges.push(makeEdge(fromSub, line, 'transmission', line.capacityMw, closesRing))
      edges.push(makeEdge(line, toSub, 'transmission', line.capacityMw, closesRing))
    }

    // ── Distribution and service ───────────────────────────────────────────
    for (const [siteCode, siteAssets] of bySite) {
      const substation = substationBySite.get(siteCode)
      if (!substation) continue

      const transformers = siteAssets.filter((a) => a.assetTypeKey === 'transformer')
      for (const transformer of transformers) {
        // Two or more transformers on a bus back each other up; a single one does not.
        edges.push(
          makeEdge(substation, transformer, 'distribution', transformer.capacityMw, transformers.length > 1),
        )
      }

      const loads = siteAssets.filter((a) => LOAD_KINDS.has(a.assetTypeKey))
      loads.forEach((load, index) => {
        const parent = transformers.length > 0 ? transformers[index % transformers.length] : substation
        edges.push(makeEdge(parent, load, 'service', load.capacityMw, transformers.length > 1))
      })
    }
  }

  // ── Flow shares ──────────────────────────────────────────────────────────
  // How the outgoing flow of a node divides between its edges: proportional to the
  // downstream capacity each edge serves, which is how load actually splits between
  // parallel paths of similar impedance.
  const out = new Map<string, GraphEdge[]>()
  const incoming = new Map<string, GraphEdge[]>()
  for (const edge of edges) {
    const o = out.get(edge.from)
    if (o) o.push(edge)
    else out.set(edge.from, [edge])
    const i = incoming.get(edge.to)
    if (i) i.push(edge)
    else incoming.set(edge.to, [edge])
  }
  for (const [, group] of out) {
    const total = group.reduce((sum, e) => sum + e.capacityMw, 0)
    for (const edge of group) {
      edge.shareOfFlow = total > 0 ? round(edge.capacityMw / total, 4) : round(1 / group.length, 4)
    }
  }

  // ── Downstream reach ─────────────────────────────────────────────────────
  const assetByCode = new Map(sorted.map((a) => [a.code, a]))
  const downstream = new Map<string, { customers: number; mw: number }>()
  for (const asset of sorted) {
    // Breadth-first over outgoing edges with a visited set: the transmission ring is a
    // genuine cycle, so an unguarded walk would not terminate.
    const seen = new Set<string>([asset.code])
    const queue = [asset.code]
    let customers = asset.customersServed
    let mw = 0
    while (queue.length > 0) {
      const current = queue.shift()!
      for (const edge of out.get(current) ?? []) {
        if (seen.has(edge.to)) continue
        seen.add(edge.to)
        const next = assetByCode.get(edge.to)
        if (!next) continue
        customers += next.customersServed
        if (LOAD_KINDS.has(next.assetTypeKey) || next.assetTypeKey === 'transformer') {
          mw += next.capacityMw
        }
        queue.push(edge.to)
      }
    }
    downstream.set(asset.code, { customers, mw: round(mw, 1) })
  }

  const totalCustomers = sorted.reduce((sum, a) => sum + a.customersServed, 0)
  const nodes: GraphNode[] = sorted.map((asset) => {
    const reach = downstream.get(asset.code) ?? { customers: asset.customersServed, mw: 0 }
    return {
      ...asset,
      isSupply: SUPPLY_KINDS.has(asset.assetTypeKey),
      isLoad: LOAD_KINDS.has(asset.assetTypeKey),
      downstreamCustomers: reach.customers,
      downstreamMw: reach.mw,
      centrality: totalCustomers > 0 ? round(clamp(reach.customers / totalCustomers, 0, 1), 5) : 0,
    }
  })

  return {
    nodes,
    edges,
    byCode: new Map(nodes.map((n) => [n.code, n])),
    out,
    in: incoming,
    totalCustomers,
  }
}

/** Direct upstream feeders of a node. */
export function parentsOf(graph: GridGraph, code: string): GraphNode[] {
  return (graph.in.get(code) ?? [])
    .map((edge) => graph.byCode.get(edge.from))
    .filter((n): n is GraphNode => Boolean(n))
}

/** Direct downstream dependants of a node. */
export function childrenOf(graph: GridGraph, code: string): GraphNode[] {
  return (graph.out.get(code) ?? [])
    .map((edge) => graph.byCode.get(edge.to))
    .filter((n): n is GraphNode => Boolean(n))
}

/**
 * Peers that can absorb load when `code` stops carrying it: the other children of the
 * same parents, of the same kind. This is the set a real switching plan draws from.
 */
export function siblingsOf(graph: GridGraph, code: string): GraphNode[] {
  const node = graph.byCode.get(code)
  if (!node) return []
  const seen = new Set<string>()
  const out: GraphNode[] = []
  for (const parent of parentsOf(graph, code)) {
    for (const child of childrenOf(graph, parent.code)) {
      if (child.code === code || seen.has(child.code)) continue
      if (child.assetTypeKey !== node.assetTypeKey) continue
      seen.add(child.code)
      out.push(child)
    }
  }
  return out.sort((a, b) => a.code.localeCompare(b.code))
}

/** Nodes ranked by how much of the network depends on them. */
export function rankByCentrality(graph: GridGraph, limit = 10): GraphNode[] {
  return [...graph.nodes].sort((a, b) => b.centrality - a.centrality).slice(0, limit)
}
