import { solveAcPowerFlow, type AcPowerFlowResult } from './ac-power-flow'
import { buildNetwork, connectedSubnetwork, type BuildOptions, type ModelAsset, type ModelEdge } from './network-model'
import { round } from './rng'

/**
 * N-1 and N-2 contingency analysis (5.0 §13, §14).
 *
 * The question a control room asks continuously and a dashboard almost never answers:
 * *if any single thing fails right now, does the grid still stand?* Each candidate is
 * removed, the network is re-solved, and the result is compared against the base case.
 *
 * Three commitments:
 *
 * **A contingency that will not solve is a finding, not an omission.** When removing a
 * branch leaves a case the solver cannot converge on, that is reported as
 * `no_solution` — which in a real network usually means voltage collapse, and is the most
 * serious outcome the scan can produce, not a gap in the results.
 *
 * **Unserved load is measured, not inferred.** Islanding is found by walking the graph
 * from the reference, and the megawatts inside the island are added up. A load that is
 * still connected but sitting at 0.7 pu is a voltage violation, not unserved.
 *
 * **N-2 is never automatic.** The pair space is quadratic and the scan is expensive; §14
 * says on-demand and it is on-demand, with an explicit budget that is reported whether or
 * not it was exhausted.
 */

export type ContingencyOutcome =
  | 'secure'
  | 'overload'
  | 'voltage_violation'
  | 'unserved_load'
  | 'islanded'
  | 'no_solution'

export interface ContingencyResult {
  /** The asset codes taken out. One for N-1, two for N-2. */
  outage: string[]
  label: string
  outcome: ContingencyOutcome
  /** True only when the case solved and breached nothing. */
  secure: boolean
  solverStatus: string
  converged: boolean
  iterations: number
  /** Worst branch loading in the post-contingency case, per cent. */
  worstLoadingPct: number
  worstLoadingBranch: string | null
  /** Furthest bus from nominal, per unit. */
  worstVoltagePu: number
  worstVoltageBus: string | null
  overloadCount: number
  voltageViolationCount: number
  /** Demand inside any island the reference cannot reach. */
  unservedMw: number
  islandedAssets: string[]
  /** Post-contingency losses, for comparison against the base case. */
  lossMw: number
  /**
   * 0–100. Combines how far past its rating the worst path goes, how far the worst bus
   * departs from nominal, and how much load is left unserved. A case that does not solve
   * scores the maximum, because a collapse is not a mild finding.
   */
  severity: number
}

export interface ContingencyReport {
  /** Always stated: this analysis is only as good as the solver behind it. */
  method: 'newton_raphson'
  baseConverged: boolean
  baseSolverStatus: string
  baseLossMw: number
  /** N-1 unless a pair scan was asked for. */
  depth: 1 | 2
  scanned: number
  candidates: number
  budgetExhausted: boolean
  results: ContingencyResult[]
  /** Cases that are not secure, worst first. */
  violations: ContingencyResult[]
  secureCount: number
  /** True when every single credible outage leaves the grid standing. */
  nMinusOneSecure: boolean
  note: string
}

export interface ContingencyOptions extends BuildOptions {
  /** Restrict the scan to these asset codes. Defaults to everything worth removing. */
  candidates?: string[]
  /** Cap on cases evaluated. A national N-2 scan is quadratic and must be bounded. */
  maxCases?: number
  depth?: 1 | 2
  tolerance?: number
  maxIterations?: number
}

/** Asset kinds whose loss is a credible single contingency. */
const CREDIBLE = new Set([
  'transformer',
  'transmission_line',
  'substation',
  'power_plant',
  'battery_storage',
  'solar_farm',
  'wind_farm',
])

const V_MIN_PU = 0.94
const V_MAX_PU = 1.06

function severityOf(result: Omit<ContingencyResult, 'severity' | 'secure' | 'outcome'>): number {
  if (!result.converged) return 100
  const overload = Math.max(0, result.worstLoadingPct - 100)
  const voltage = Math.max(
    0,
    result.worstVoltagePu < 1 ? (V_MIN_PU - result.worstVoltagePu) * 100 : (result.worstVoltagePu - V_MAX_PU) * 100,
  )
  const unserved = result.unservedMw
  return round(Math.min(100, overload * 0.45 + voltage * 4 + unserved * 0.08), 2)
}

function outcomeOf(result: {
  converged: boolean
  unservedMw: number
  islandedAssets: string[]
  overloadCount: number
  voltageViolationCount: number
}): ContingencyOutcome {
  if (!result.converged) return 'no_solution'
  if (result.unservedMw > 0.01) return 'unserved_load'
  if (result.islandedAssets.length > 0) return 'islanded'
  if (result.overloadCount > 0) return 'overload'
  if (result.voltageViolationCount > 0) return 'voltage_violation'
  return 'secure'
}

/** Demand sitting inside the part of the network the reference cannot reach. */
function unservedIn(islanded: string[], assets: ModelAsset[]): number {
  const inIsland = new Set(islanded)
  return round(
    assets
      .filter(
        (asset) =>
          inIsland.has(asset.code) &&
          !asset.isOutage &&
          (asset.assetTypeKey === 'industrial_load' || asset.assetTypeKey === 'ev_hub'),
      )
      .reduce((sum, asset) => sum + Math.abs(asset.loadMw), 0),
    3,
  )
}

function evaluate(
  outage: string[],
  assets: ModelAsset[],
  edges: ModelEdge[],
  options: ContingencyOptions,
): ContingencyResult {
  const built = buildNetwork(assets, edges, {
    ...options,
    outages: [...(options.outages ?? []), ...outage],
  })

  const unservedMw = unservedIn(built.islanded, assets)
  const solved: AcPowerFlowResult = solveAcPowerFlow(connectedSubnetwork(built), {
    tolerance: options.tolerance,
    maxIterations: options.maxIterations ?? 20,
    enforceQLimits: true,
  })

  const worstBranch = solved.converged
    ? solved.branches.reduce<{ loadingPct: number; label: string } | null>((worst, branch) => {
        if (branch.ratingMva <= 0) return worst
        return !worst || branch.loadingPct > worst.loadingPct
          ? { loadingPct: branch.loadingPct, label: `${branch.from}→${branch.to}` }
          : worst
      }, null)
    : null

  const worstBus = solved.converged
    ? solved.buses.reduce<{ vPu: number; code: string } | null>((worst, bus) => {
        const deviation = Math.abs(bus.vPu - 1)
        return !worst || deviation > Math.abs(worst.vPu - 1) ? { vPu: bus.vPu, code: bus.code } : worst
      }, null)
    : null

  const core = {
    outage,
    label: outage.join(' + '),
    solverStatus: solved.solverStatus,
    converged: solved.converged,
    iterations: solved.iterations,
    worstLoadingPct: worstBranch?.loadingPct ?? 0,
    worstLoadingBranch: worstBranch?.label ?? null,
    worstVoltagePu: worstBus?.vPu ?? 0,
    worstVoltageBus: worstBus?.code ?? null,
    overloadCount: solved.overloads.length,
    voltageViolationCount: solved.voltageViolations.length,
    unservedMw,
    islandedAssets: built.islanded,
    lossMw: solved.converged ? solved.totalLossMw : 0,
  }

  const outcome = outcomeOf(core)
  return { ...core, outcome, secure: outcome === 'secure', severity: severityOf(core) }
}

export function runContingencyAnalysis(
  assets: ModelAsset[],
  edges: ModelEdge[],
  options: ContingencyOptions = {},
): ContingencyReport {
  const depth = options.depth ?? 1
  const maxCases = options.maxCases ?? (depth === 1 ? 200 : 120)

  // The base case first. Every comparison below is against it, and a base case that will
  // not solve makes the whole scan meaningless — so it is reported rather than papered
  // over with a hundred equally meaningless contingencies.
  const baseBuilt = buildNetwork(assets, edges, options)
  const base = solveAcPowerFlow(connectedSubnetwork(baseBuilt), {
    tolerance: options.tolerance,
    maxIterations: options.maxIterations ?? 25,
    enforceQLimits: true,
  })

  if (!base.converged) {
    return {
      method: 'newton_raphson',
      baseConverged: false,
      baseSolverStatus: base.solverStatus,
      baseLossMw: 0,
      depth,
      scanned: 0,
      candidates: 0,
      budgetExhausted: false,
      results: [],
      violations: [],
      secureCount: 0,
      nMinusOneSecure: false,
      note: `The base case did not solve (${base.solverStatus}). A contingency scan against an unsolved base would be comparing two things that are both unknown.`,
    }
  }

  const pool = (
    options.candidates ??
    assets.filter((asset) => CREDIBLE.has(asset.assetTypeKey) && !asset.isOutage).map((asset) => asset.code)
  )
    // Deterministic order so a truncated scan always truncates the same way.
    .slice()
    .sort()

  const cases: string[][] = []
  if (depth === 1) {
    for (const code of pool) cases.push([code])
  } else {
    for (let i = 0; i < pool.length; i += 1) {
      for (let j = i + 1; j < pool.length; j += 1) cases.push([pool[i], pool[j]])
    }
  }

  const budgetExhausted = cases.length > maxCases
  const scanned = budgetExhausted ? cases.slice(0, maxCases) : cases

  const results = scanned
    .map((outage) => evaluate(outage, assets, edges, options))
    .sort((a, b) => b.severity - a.severity)

  const violations = results.filter((result) => !result.secure)

  return {
    method: 'newton_raphson',
    baseConverged: true,
    baseSolverStatus: base.solverStatus,
    baseLossMw: base.totalLossMw,
    depth,
    scanned: scanned.length,
    candidates: cases.length,
    budgetExhausted,
    results,
    violations,
    secureCount: results.length - violations.length,
    // Only claimable when the whole space was scanned. A truncated scan that found
    // nothing has not shown the grid is secure; it has shown part of it is.
    nMinusOneSecure: depth === 1 && !budgetExhausted && violations.length === 0,
    note: budgetExhausted
      ? `Scanned ${scanned.length} of ${cases.length} cases against a budget of ${maxCases}. This is a partial scan and cannot establish security.`
      : violations.length === 0
        ? `All ${results.length} single contingencies leave the network within limits.`
        : `${violations.length} of ${results.length} contingencies breach a limit.`,
  }
}
