import { round } from './rng'
import type { ModelAsset, ModelEdge } from './network-model'

/**
 * The electrical model validator (5.0 §2, §3).
 *
 * A load flow will happily solve a network that could not exist. This checks the model
 * *before* the solver runs and says what is wrong with it — a transformer with no rating,
 * a load larger than everything feeding it, two assets at incompatible voltages wired
 * directly together, an asset nothing supplies.
 *
 * Two rules govern the whole file:
 *
 * **It never corrects anything.** Every finding is a report. Silently repairing a model
 * would mean the operator's data says one thing and the platform's answer is based on
 * another, and the difference would be invisible exactly when it mattered.
 *
 * **A finding is not a fault.** `ERROR` means the model cannot be solved as it stands.
 * `WARNING` means it will solve but the result should be read with care. `INFO` is a fact
 * worth surfacing. Nothing here says the *grid* is broken; it says the *model* is.
 */

export type Severity = 'error' | 'warning' | 'info'

export type FindingCode =
  | 'missing_rating'
  | 'missing_impedance'
  | 'missing_length'
  | 'voltage_mismatch'
  | 'no_voltage_level'
  | 'isolated_asset'
  | 'no_source'
  | 'duplicate_connection'
  | 'self_connection'
  | 'unknown_endpoint'
  | 'impossible_load'
  | 'transformer_undersized'
  | 'feeder_undersized'
  | 'cable_undersized'
  | 'reverse_transformer'
  | 'circular_topology'
  | 'load_exceeds_source'

export interface Finding {
  code: FindingCode
  severity: Severity
  /** The asset or `from→to` this is about. */
  subject: string
  message: string
  messageAr: string
  /** The numbers behind the judgement, so it can be checked rather than believed. */
  detail: Record<string, number | string>
}

export interface ValidationReport {
  findings: Finding[]
  errors: number
  warnings: number
  infos: number
  /** False when any ERROR is present: the model should not be solved as it stands. */
  solvable: boolean
  assetsChecked: number
  branchesChecked: number
}

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

/** Voltage classes that may legitimately sit on the same busbar without a transformer. */
function sameClass(a: number, b: number): boolean {
  if (a <= 0 || b <= 0) return false
  const ratio = Math.max(a, b) / Math.min(a, b)
  return ratio < 1.1
}

function finding(
  code: FindingCode,
  severity: Severity,
  subject: string,
  message: string,
  messageAr: string,
  detail: Record<string, number | string> = {},
): Finding {
  return { code, severity, subject, message, messageAr, detail }
}

export function validateGridModel(assets: ModelAsset[], edges: ModelEdge[]): ValidationReport {
  const findings: Finding[] = []
  const byCode = new Map(assets.map((asset) => [asset.code, asset]))

  // ── Per-asset checks ──────────────────────────────────────────────────────
  for (const asset of assets) {
    if (asset.voltageKv <= 0.01) {
      findings.push(
        finding(
          'no_voltage_level',
          'error',
          asset.code,
          'No voltage level, so this asset has no per-unit base and cannot appear in a load flow.',
          'لا يوجد مستوى جهد، فلا أساس نسبي لهذا الأصل ولا يمكن إدراجه في سريان الحمل.',
          { voltageKv: asset.voltageKv },
        ),
      )
    }

    if (asset.capacityMw <= 0) {
      findings.push(
        finding(
          'missing_rating',
          'error',
          asset.code,
          'No capacity rating, so loading cannot be expressed as a fraction of anything.',
          'لا يوجد تصنيف قدرة، فلا يمكن التعبير عن التحميل كنسبة من شيء.',
          { capacityMw: asset.capacityMw },
        ),
      )
    }

    // A reading beyond any plausible emergency rating is a data fault, not an overload.
    if (asset.capacityMw > 0 && Math.abs(asset.loadPct) > 300) {
      findings.push(
        finding(
          'impossible_load',
          'error',
          asset.code,
          'Loading beyond any plausible emergency rating. This is a data fault, not an overload.',
          'تحميل يتجاوز أي تصنيف طوارئ معقول. هذا خلل في البيانات لا تحميل زائد.',
          { loadPct: round(asset.loadPct, 1), capacityMw: asset.capacityMw },
        ),
      )
    }
  }

  // ── Per-branch checks ─────────────────────────────────────────────────────
  const seen = new Set<string>()
  for (const edge of edges) {
    const key = `${edge.from}→${edge.to}`
    const reverseKey = `${edge.to}→${edge.from}`

    if (edge.from === edge.to) {
      findings.push(
        finding(
          'self_connection',
          'error',
          key,
          'An asset connected to itself.',
          'أصل موصول بنفسه.',
        ),
      )
      continue
    }

    if (seen.has(key) || seen.has(reverseKey)) {
      findings.push(
        finding(
          'duplicate_connection',
          'warning',
          key,
          'This pair is already connected. A genuine parallel circuit is fine; a duplicated record is not, and the two look identical here.',
          'هذا الزوج موصول بالفعل. الدائرة المتوازية الحقيقية مقبولة، أما السجل المكرر فلا، وهما متطابقان هنا.',
        ),
      )
    }
    seen.add(key)

    const from = byCode.get(edge.from)
    const to = byCode.get(edge.to)

    if (!from || !to) {
      findings.push(
        finding(
          'unknown_endpoint',
          'error',
          key,
          'One end of this connection is not an asset in the model.',
          'أحد طرفي هذا الاتصال ليس أصلًا في النموذج.',
          { missing: !from ? edge.from : edge.to },
        ),
      )
      continue
    }

    if (edge.capacityMw <= 0) {
      findings.push(
        finding(
          'missing_rating',
          'warning',
          key,
          'No thermal rating on this path, so utilisation on it cannot be computed.',
          'لا تصنيف حراري لهذا المسار، فلا يمكن حساب استغلاله.',
        ),
      )
    }

    if (edge.lengthKm <= 0) {
      findings.push(
        finding(
          'missing_length',
          'warning',
          key,
          'No length, so series impedance falls back to a floor rather than being derived.',
          'لا يوجد طول، فتُستخدم قيمة دنيا للممانعة بدلًا من اشتقاقها.',
          { lengthKm: edge.lengthKm },
        ),
      )
    }

    // Two different voltages wired together with no transformer between them.
    const transformerBetween =
      from.assetTypeKey === 'transformer' || to.assetTypeKey === 'transformer'
    if (!transformerBetween && !sameClass(from.voltageKv, to.voltageKv)) {
      findings.push(
        finding(
          'voltage_mismatch',
          'error',
          key,
          'Two different voltage levels joined with no transformer between them.',
          'مستويان مختلفان من الجهد موصولان دون محول بينهما.',
          { fromKv: from.voltageKv, toKv: to.voltageKv },
        ),
      )
    }

    // A transformer whose high side faces the load and whose low side faces the source.
    if (from.assetTypeKey === 'transformer' || to.assetTypeKey === 'transformer') {
      const transformer = from.assetTypeKey === 'transformer' ? from : to
      const other = from.assetTypeKey === 'transformer' ? to : from
      if (SOURCE_TYPES.has(other.assetTypeKey) && other.voltageKv < transformer.voltageKv * 0.5) {
        findings.push(
          finding(
            'reverse_transformer',
            'warning',
            key,
            'The source side of this transformer sits at a much lower voltage than its other side. Check the winding orientation.',
            'جانب المصدر لهذا المحول عند جهد أقل بكثير من جانبه الآخر. تحقق من اتجاه الملفات.',
            { sourceKv: other.voltageKv, transformerKv: transformer.voltageKv },
          ),
        )
      }
    }
  }

  // ── Connectivity ──────────────────────────────────────────────────────────
  const neighbours = new Map<string, Set<string>>()
  for (const asset of assets) neighbours.set(asset.code, new Set())
  for (const edge of edges) {
    if (!byCode.has(edge.from) || !byCode.has(edge.to)) continue
    neighbours.get(edge.from)?.add(edge.to)
    neighbours.get(edge.to)?.add(edge.from)
  }

  for (const asset of assets) {
    if ((neighbours.get(asset.code)?.size ?? 0) === 0) {
      findings.push(
        finding(
          'isolated_asset',
          'warning',
          asset.code,
          'Nothing connects to this asset. It cannot be supplied and it cannot supply anything.',
          'لا شيء يتصل بهذا الأصل. لا يمكن تغذيته ولا يمكنه تغذية شيء.',
        ),
      )
    }
  }

  const sources = assets.filter((asset) => SOURCE_TYPES.has(asset.assetTypeKey) && !asset.isOutage)
  if (assets.length > 0 && sources.length === 0) {
    findings.push(
      finding(
        'no_source',
        'error',
        'network',
        'No generation in service anywhere in this model. There is nothing for a load flow to reference.',
        'لا يوجد توليد في الخدمة في أي مكان من هذا النموذج. لا مرجع لسريان الحمل.',
        { assets: assets.length },
      ),
    )
  } else if (sources.length > 0) {
    // Every load must be reachable from some source.
    const reached = new Set<string>()
    const queue = sources.map((source) => source.code)
    for (const code of queue) reached.add(code)
    while (queue.length > 0) {
      const code = queue.shift()!
      for (const next of neighbours.get(code) ?? []) {
        if (reached.has(next)) continue
        reached.add(next)
        queue.push(next)
      }
    }
    for (const asset of assets) {
      if (reached.has(asset.code)) continue
      if ((neighbours.get(asset.code)?.size ?? 0) === 0) continue // already reported isolated
      findings.push(
        finding(
          'no_source',
          'error',
          asset.code,
          'No path from any in-service source reaches this asset.',
          'لا يوجد مسار من أي مصدر في الخدمة يصل إلى هذا الأصل.',
        ),
      )
    }
  }

  // Total demand against total in-service generation.
  const totalLoadMw = assets
    .filter((asset) => LOAD_TYPES.has(asset.assetTypeKey) && !asset.isOutage)
    .reduce((sum, asset) => sum + Math.abs(asset.loadMw), 0)
  const totalSourceMw = sources.reduce((sum, asset) => sum + asset.capacityMw, 0)

  if (totalLoadMw > totalSourceMw && totalSourceMw > 0) {
    findings.push(
      finding(
        'load_exceeds_source',
        'error',
        'network',
        'Total demand exceeds the nameplate capacity of everything generating. No load flow can serve it.',
        'إجمالي الطلب يتجاوز القدرة الاسمية لكل ما يولد. لا يمكن لأي سريان حمل تغطيته.',
        { totalLoadMw: round(totalLoadMw, 1), totalSourceMw: round(totalSourceMw, 1) },
      ),
    )
  }

  const errors = findings.filter((f) => f.severity === 'error').length
  const warnings = findings.filter((f) => f.severity === 'warning').length

  return {
    findings,
    errors,
    warnings,
    infos: findings.length - errors - warnings,
    solvable: errors === 0,
    assetsChecked: assets.length,
    branchesChecked: edges.length,
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Calibration (§3)
// ─────────────────────────────────────────────────────────────────────────────

export type CalibrationBand = 'ok' | 'tight' | 'over' | 'impossible'

export interface CalibrationEntry {
  code: string
  assetTypeKey: string
  /** What everything downstream of this asset actually draws. */
  downstreamDemandMw: number
  /** What this asset is rated to pass. */
  ratedCapacityMw: number
  /** Demand as a percentage of rating. Above 100 the seed cannot be served. */
  utilisationPct: number
  band: CalibrationBand
  /** Megawatts by which the rating falls short. Zero when it does not. */
  shortfallMw: number
  downstreamAssets: number
}

export interface CalibrationReport {
  entries: CalibrationEntry[]
  worst: CalibrationEntry | null
  overCount: number
  impossibleCount: number
  /** 0–100. A model whose ratings cannot serve its own loads scores low, and should. */
  calibrationRisk: number
  totalShortfallMw: number
  note: string
}

function band(utilisationPct: number): CalibrationBand {
  if (utilisationPct > 200) return 'impossible'
  if (utilisationPct > 100) return 'over'
  if (utilisationPct > 85) return 'tight'
  return 'ok'
}

/**
 * Compare what each asset is asked to carry against what it is rated to carry.
 *
 * This is the analysis that found the 4.2 discrepancy: a 63 MW transformer with 279 MW of
 * demand behind it, and a 987 MW load fed through a 606 MW unit. It reports the gap; it
 * does not close it. Changing a seeded rating moves every calibrated figure in the demo,
 * and that is an operator's decision to take deliberately.
 */
export function analyseCalibration(assets: ModelAsset[], edges: ModelEdge[]): CalibrationReport {
  const byCode = new Map(assets.map((asset) => [asset.code, asset]))

  const outgoing = new Map<string, string[]>()
  for (const edge of edges) {
    if (!byCode.has(edge.from) || !byCode.has(edge.to)) continue
    const list = outgoing.get(edge.from)
    if (list) list.push(edge.to)
    else outgoing.set(edge.from, [edge.to])
  }

  /** Sink demand below a node, cycle-guarded. */
  const cache = new Map<string, { mw: number; count: number }>()
  function below(code: string, seen: Set<string>): { mw: number; count: number } {
    const cached = cache.get(code)
    if (cached) return cached
    if (seen.has(code)) return { mw: 0, count: 0 }
    seen.add(code)

    const asset = byCode.get(code)
    let mw = asset && LOAD_TYPES.has(asset.assetTypeKey) && !asset.isOutage ? Math.abs(asset.loadMw) : 0
    let count = 1
    for (const next of outgoing.get(code) ?? []) {
      const child = below(next, seen)
      mw += child.mw
      count += child.count
    }

    seen.delete(code)
    const result = { mw: round(mw, 3), count }
    if (seen.size === 0) cache.set(code, result)
    return result
  }

  const entries: CalibrationEntry[] = assets
    // Only assets that actually carry: a load is not miscalibrated against itself.
    .filter((asset) => !LOAD_TYPES.has(asset.assetTypeKey) && !SOURCE_TYPES.has(asset.assetTypeKey))
    .map((asset) => {
      const downstream = below(asset.code, new Set())
      const rated = Math.max(0.001, asset.capacityMw)
      const utilisationPct = round((downstream.mw / rated) * 100, 2)
      return {
        code: asset.code,
        assetTypeKey: asset.assetTypeKey,
        downstreamDemandMw: downstream.mw,
        ratedCapacityMw: asset.capacityMw,
        utilisationPct,
        band: band(utilisationPct),
        shortfallMw: round(Math.max(0, downstream.mw - rated), 3),
        downstreamAssets: Math.max(0, downstream.count - 1),
      }
    })
    .sort((a, b) => b.utilisationPct - a.utilisationPct)

  const overCount = entries.filter((entry) => entry.band === 'over' || entry.band === 'impossible').length
  const impossibleCount = entries.filter((entry) => entry.band === 'impossible').length
  const totalShortfallMw = round(entries.reduce((sum, entry) => sum + entry.shortfallMw, 0), 2)

  // Weighted towards how badly the worst offender misses, not just how many miss: one
  // asset at 450 % is a more serious calibration problem than ten at 105 %.
  const worst = entries[0] ?? null
  const worstExcess = worst ? Math.max(0, worst.utilisationPct - 100) : 0
  const share = entries.length > 0 ? overCount / entries.length : 0
  const calibrationRisk = round(Math.min(100, worstExcess * 0.25 + share * 100 * 0.6), 1)

  return {
    entries,
    worst,
    overCount,
    impossibleCount,
    calibrationRisk,
    totalShortfallMw,
    note:
      overCount === 0
        ? 'Every carrying asset is rated for the demand behind it.'
        : `${overCount} assets carry more demand than their rating. The figures are reported, not corrected — changing a seeded rating moves every calibrated number in the demonstration.`,
  }
}
