import 'server-only'

import { cache } from 'react'

import { prisma } from '../db'
import {
  buildGridGraph,
  type GraphAsset,
  type GridGraph,
} from '../engine/graph'
import type { AssetSnapshot } from '../engine/grid'
import type { CascadeAssetState } from '../engine/cascade'

/**
 * The grid graph, read once and kept.
 *
 * Topology changes when an asset is commissioned or retired — on the order of months —
 * while the cascade engine is asked to walk it on every page load. The same two-layer
 * caching the fleet read uses applies here for the same reason, with a longer TTL
 * because the underlying data moves more slowly still.
 */

interface TtlEntry<T> {
  value: T
  expiresAt: number
}

const store = globalThis as unknown as { nabdhGraphCache?: TtlEntry<GridGraph> }
const GRAPH_TTL_MS = 120_000

const GRAPH_SELECT = {
  code: true,
  name: true,
  nameAr: true,
  lat: true,
  lon: true,
  capacityMw: true,
  voltageKv: true,
  customersServed: true,
  assetType: { select: { key: true } },
  region: { select: { code: true } },
  site: { select: { code: true } },
} as const

export const loadGridGraph = cache(async (): Promise<GridGraph> => {
  const cached = store.nabdhGraphCache
  if (cached && cached.expiresAt > Date.now()) return cached.value

  const rows = await prisma.asset.findMany({ select: GRAPH_SELECT })
  const assets: GraphAsset[] = rows.map((row) => ({
    code: row.code,
    name: row.name,
    nameAr: row.nameAr,
    assetTypeKey: row.assetType.key,
    regionCode: row.region.code,
    siteCode: row.site.code,
    lat: row.lat,
    lon: row.lon,
    capacityMw: row.capacityMw,
    voltageKv: row.voltageKv,
    customersServed: row.customersServed,
  }))

  const graph = buildGridGraph(assets)
  store.nabdhGraphCache = { value: graph, expiresAt: Date.now() + GRAPH_TTL_MS }
  return graph
})

export function invalidateGraphCache() {
  store.nabdhGraphCache = undefined
}

/** The loading state the cascade walk starts from, keyed by asset code. */
export function cascadeStates(assets: AssetSnapshot[]): Map<string, CascadeAssetState> {
  return new Map(
    assets.map((asset) => [
      asset.profile.code,
      {
        code: asset.profile.code,
        loadMw: Math.max(0, asset.sample.loadMw),
        capacityMw: asset.profile.capacityMw,
        loadPct: asset.sample.loadPct,
        isOutage: asset.sample.isOutage,
      },
    ]),
  )
}

/**
 * Persist the graph into `grid_nodes` / `grid_edges`.
 *
 * The runtime reads the graph from the asset register, not from these tables — they are
 * a materialised snapshot so a stored cascade run can be read back against the topology
 * it was computed on. Called from the seed; safe to call again, it replaces wholesale.
 */
export async function persistGridGraph(graph: GridGraph): Promise<{ nodes: number; edges: number }> {
  await prisma.gridEdge.deleteMany()
  await prisma.gridNode.deleteMany()

  const assets = await prisma.asset.findMany({ select: { id: true, code: true } })
  const assetIdByCode = new Map(assets.map((asset) => [asset.code, asset.id]))

  await prisma.gridNode.createMany({
    data: graph.nodes.map((node) => ({
      code: node.code,
      assetId: assetIdByCode.get(node.code) ?? null,
      regionCode: node.regionCode,
      kind: node.assetTypeKey,
      name: node.name,
      nameAr: node.nameAr,
      lat: node.lat,
      lon: node.lon,
      capacityMw: node.capacityMw,
      voltageKv: node.voltageKv,
      centrality: node.centrality,
      customers: node.customersServed,
      isSupply: node.isSupply,
      isLoad: node.isLoad,
    })),
  })

  const stored = await prisma.gridNode.findMany({ select: { id: true, code: true } })
  const nodeIdByCode = new Map(stored.map((node) => [node.code, node.id]))

  const edges = graph.edges
    .map((edge) => {
      const fromNodeId = nodeIdByCode.get(edge.from)
      const toNodeId = nodeIdByCode.get(edge.to)
      if (!fromNodeId || !toNodeId) return null
      return {
        code: edge.code,
        fromNodeId,
        toNodeId,
        kind: edge.kind,
        capacityMw: edge.capacityMw,
        lengthKm: edge.lengthKm,
        impedance: edge.impedance,
        shareOfFlow: edge.shareOfFlow,
        isRedundant: edge.isRedundant,
      }
    })
    .filter((edge): edge is NonNullable<typeof edge> => edge !== null)

  await prisma.gridEdge.createMany({ data: edges })
  return { nodes: graph.nodes.length, edges: edges.length }
}
