import 'server-only'

import { prisma } from '../db'
import { REGIONS, getRegion } from '../data/regions'
import { matchDna, type DnaMatch } from '../engine/dna'
import {
  environmentFor,
  extractAssetDna,
  resourcesFor,
  maintenanceDebt,
  assetAgeYears,
  type AssetSnapshot,
  type DnaPatternRef,
  type GridSnapshot,
} from '../engine/grid'
import { predictFailure, type PredictionResult } from '../engine/prediction'
import {
  evaluateCandidates,
  generateCandidates,
  type ActionContext,
  type ActionResources,
  type EvaluatedAction,
} from '../engine/actions'
import {
  NO_PERTURBATION,
  remainingUsefulLifeDays,
  sampleAsset,
  type AppliedAction,
  type Perturbation,
} from '../engine/telemetry'
import { detectWeakSignals, weakSignalLeadMinutes, type WeakSignalResult } from '../engine/weak-signal'
import { simulateCascade, type CascadeResult } from '../engine/cascade'
import { generateStrategies, optimiseStrategies, type OptimizationResult } from '../engine/optimize'
import { analyseRootCause, type RootCauseResult } from '../engine/rootcause'
import { orchestrate, type FutureBranch, type Orchestration } from '../engine/orchestrator'
import { AGENT_DEFS, runAgents, type AgentContext, type AgentResult } from '../engine/agents'
import type { GridGraph } from '../engine/graph'
import { getGridSnapshot, loadDnaPatterns, loadRiskWeights } from './grid-service'
import { cascadeStates, loadGridGraph } from './graph-service'
import {
  assetEnvelope,
  physicsPlausibility,
  validatePhysics,
  type OperatingEnvelope,
  type PhysicsValidation,
} from '../engine/physics'
import { estimateUncertainty, type UncertaintyResult } from '../engine/uncertainty'
import {
  buildCascadeTree,
  findCascadeBreakpoints,
  type CascadeBreakpoint,
  type CascadeTreeNode,
} from '../engine/cascade-tree'
import { getAssetDataQuality, type AssetDataQuality } from './quality-service'

/**
 * The analysis pipeline behind the AI Operations Room, the War Room and every 2.0 page.
 *
 * One pass builds everything the eight agents need — the live snapshot, the forward
 * prediction, the DNA match, the weak-signal reading, the cascade walk, the action set,
 * the optimisation and the root-cause analysis — and hands it to them as a single
 * immutable context. Doing it once matters: an agent panel where each agent fetched its
 * own view of "now" would be a panel of agents quietly disagreeing about the time.
 */

const MS_PER_MIN = 60_000
const DEFAULT_HORIZON_MIN = 180

export interface IntelligenceOptions {
  ts?: number
  perturbation?: Perturbation
  objectiveKey?: string
  horizonMin?: number
  /** Walk backwards to measure the weak-signal lead time. Costly; off by default. */
  includeLeadTime?: boolean
}

export interface AssetIntelligence {
  ts: number
  assetCode: string
  snapshot: GridSnapshot
  graph: GridGraph
  focus: AssetSnapshot
  prediction: PredictionResult
  dnaMatches: Array<DnaMatch<DnaPatternRef>>
  weakSignals: WeakSignalResult
  cascade: CascadeResult
  actions: EvaluatedAction[]
  optimisation: OptimizationResult
  rootCause: RootCauseResult
  dataQuality: AssetDataQuality
  agents: AgentResult[]
  orchestration: Orchestration
  resources: ActionResources
  rulDays: number
  actionContext: ActionContext
  perturbation: Perturbation
  leadTime: { patternAtMin: number | null; alarmAtMin: number | null; leadMinutes: number } | null
  /** 4.0 — the operating envelope this asset is actually working inside (§12, §13). */
  envelope: OperatingEnvelope
  /** 4.0 — the recommended strategy measured against the engineering limits (§10, §11). */
  physics: PhysicsValidation
  /** 4.0 — the belief around the prediction rather than a single number (§14, §15). */
  uncertainty: UncertaintyResult
  /** 4.0 — the cascade as a branching tree, with the cheapest place to cut it (§43, §45). */
  cascadeTree: CascadeTreeNode | null
  breakpoints: CascadeBreakpoint[]
}

/** Maintenance facts the maintenance agent reads, in one query. */
async function loadMaintenanceContext(assetCode: string, ts: number) {
  const rows = await prisma.maintenanceRecord.findMany({
    where: { asset: { code: assetCode } },
    orderBy: { scheduledAt: 'desc' },
    take: 6,
    select: {
      status: true,
      performedAt: true,
      scheduledAt: true,
      findings: true,
      findingsAr: true,
    },
  })

  const last = rows.find((row) => row.performedAt !== null)
  const open = rows.filter((row) => row.status !== 'completed' && row.status !== 'cancelled').length

  return {
    lastPerformedAt: last?.performedAt?.getTime() ?? null,
    openWorkOrders: open,
    lastFindings: last?.findings ?? '',
    lastFindingsAr: last?.findingsAr ?? '',
    ts,
  }
}

/**
 * Renewable context for the region the asset sits in.
 *
 * The deviation is measured against the same environment with the scenario removed,
 * which is what makes "output is 18 % below the model" a statement about the scenario
 * rather than about the time of day.
 */
function renewableContext(snapshot: GridSnapshot, regionCode: string, perturbation: Perturbation) {
  const region = snapshot.regions.find((entry) => entry.code === regionCode)
  const solarDelta = perturbation.solarDeltaPct ?? 0
  const windDelta = perturbation.windDeltaPct ?? 0
  const applies = !perturbation.regionCode || perturbation.regionCode === regionCode
  const share = region?.renewableSharePct ?? snapshot.renewableSharePct

  return {
    expectedMw: region?.renewableMw ?? snapshot.renewableMw,
    sharePct: share,
    variabilityPct: region?.renewableVariabilityPct ?? snapshot.renewableVariabilityPct,
    deviationPct: applies ? (solarDelta * 0.75 + windDelta * 0.25) : 0,
    cloudPct: region?.env.cloudCoverPct ?? 0,
  }
}

/**
 * Run the full pipeline for one asset.
 *
 * Returns null when the code is unknown, which the routes turn into a 404 rather than an
 * empty analysis that looks like a healthy one.
 */
export async function analyseAsset(
  assetCode: string,
  options: IntelligenceOptions = {},
): Promise<AssetIntelligence | null> {
  const ts = options.ts ?? Date.now()
  const perturbation = options.perturbation ?? NO_PERTURBATION
  const horizonMin = options.horizonMin ?? DEFAULT_HORIZON_MIN

  const [snapshot, graph, patterns, weights, dataQuality, maintenance] = await Promise.all([
    getGridSnapshot({ ts, perturbation }),
    loadGridGraph(),
    loadDnaPatterns(),
    loadRiskWeights(),
    getAssetDataQuality(assetCode, { now: ts }),
    loadMaintenanceContext(assetCode, ts),
  ])

  const focus = snapshot.assets.find((entry) => entry.profile.code === assetCode)
  if (!focus) return null

  const region = getRegion(focus.profile.regionCode)
  const envAt = environmentFor(region, perturbation)
  const env = envAt(ts)

  const prediction = predictFailure(focus.profile, ts, envAt, {
    perturbation,
    dnaSimilarity: focus.dnaSimilarity,
    neighbourStress: focus.neighbourStress,
    weights,
    horizonMin,
    stepMin: 5,
  })

  const regionSnapshot = snapshot.regions.find((entry) => entry.code === region.code)
  const currentDna = extractAssetDna({
    profile: focus.profile,
    ts,
    sample: focus.sample,
    env,
    envAt,
    perturbation,
    neighbourStress: focus.neighbourStress,
    renewableVariabilityPct: regionSnapshot?.renewableVariabilityPct ?? 0,
  })
  const dnaMatches = matchDna(currentDna, patterns, 3)

  const baseline = sampleAsset(focus.profile, ts, env, NO_PERTURBATION)
  const weakSignals = detectWeakSignals(focus.profile, focus.sample, env, { baseline })

  const cascade = simulateCascade(graph, cascadeStates(snapshot.assets), assetCode, {
    horizonMin: 60,
  })

  const resources = resourcesFor(focus.profile, snapshot.assets)
  const actionContext: ActionContext = {
    profile: focus.profile,
    ts,
    envAt,
    perturbation,
    dnaSimilarity: focus.dnaSimilarity,
    neighbourStress: focus.neighbourStress,
    weights,
    resources,
    evaluateAtMin: prediction.etaMinutes ?? 45,
  }
  const actions = evaluateCandidates(actionContext, generateCandidates(actionContext))
  const optimisation = optimiseStrategies(actionContext, generateStrategies(actionContext, actions), {
    objectiveKey: options.objectiveKey,
  })

  const envNormal = environmentFor(region, NO_PERTURBATION)(ts)
  const renewable = renewableContext(snapshot, region.code, perturbation)
  const rootCause = analyseRootCause({
    profile: focus.profile,
    sample: focus.sample,
    baseline,
    env,
    envNormal,
    weakSignals,
    prediction,
    maintenanceDebtDays: maintenanceDebt(focus.profile, ts),
    ageYears: assetAgeYears(focus.profile, ts),
    neighbourStress: focus.neighbourStress,
    dnaSimilarity: focus.dnaSimilarity,
    renewableDeviationPct: renewable.deviationPct,
  })

  const rulDays = remainingUsefulLifeDays(focus.profile, focus.sample)

  const agentContext: AgentContext = {
    ts,
    snapshot,
    graph,
    focus,
    prediction,
    dnaMatches,
    weakSignals,
    cascade,
    actions,
    optimisation,
    rootCause,
    dataQuality,
    rulDays,
    maintenance: {
      lastPerformedAt: maintenance.lastPerformedAt,
      debtDays: maintenanceDebt(focus.profile, ts),
      openWorkOrders: maintenance.openWorkOrders,
      lastFindings: maintenance.lastFindings,
      lastFindingsAr: maintenance.lastFindingsAr,
    },
    renewable,
    resources: {
      transferCapacityMw: resources.transferCapacityMw,
      batteryPowerMw: resources.batteryPowerMw,
      flexibleLoadMw: resources.flexibleLoadMw,
    },
  }

  const agents = runAgents(agentContext)
  const orchestration = orchestrate(agentContext, agents)

  const leadTime = options.includeLeadTime
    ? weakSignalLeadMinutes(focus.profile, ts, envAt, perturbation, { lookBackMin: 180, stepMin: 2 })
    : null

  // ── 4.0: physics, uncertainty and the cascade tree ────────────────────────
  //
  // Ordered deliberately. The envelope describes where the asset is; the validator
  // measures the recommendation against the limits; physics plausibility then feeds the
  // uncertainty engine, so a prediction the physics cannot support cannot carry a high
  // confidence into the UI (§9, §14).
  const envelope = assetEnvelope({
    profile: focus.profile,
    sample: focus.sample,
    ageYears: assetAgeYears(focus.profile, ts),
    maintenanceDebt: Math.min(1, maintenanceDebt(focus.profile, ts) / 365),
  })

  const recommended = optimisation.strategies.find((strategy) => strategy.isRecommended)
  const physics = validatePhysics({
    profile: focus.profile,
    after: sampleAsset(focus.profile, ts, env, {
      ...perturbation,
      actions: recommended?.actions ?? [],
    }),
    actions: recommended?.actions ?? [],
    resources: {
      transferCapacityMw: resources.transferCapacityMw,
      batteryPowerMw: resources.batteryPowerMw,
      flexibleLoadMw: resources.flexibleLoadMw,
    },
    reserveMarginPct: regionSnapshot?.reserveMarginPct ?? snapshot.reserveMarginPct,
    // The demo grid holds frequency tightly; the deviation is derived from the national
    // figure rather than invented per asset.
    frequencyDeviationHz: Math.abs(snapshot.frequencyHz - 60),
    unavailableTargets: (recommended?.actions ?? [])
      .map((action) => action.assetCode)
      .filter((code) => {
        const target = snapshot.assets.find((entry) => entry.profile.code === code)
        return Boolean(target && (target.sample.isOutage || target.profile.status !== 'online'))
      }),
  })

  // The forward walk's own worst frame, rather than a separate peak figure: the
  // plausibility question is about the trajectory the model actually produced.
  const peakFrame = prediction.frames.reduce(
    (worst, frame) => (Math.abs(frame.sample.loadPct) > Math.abs(worst.sample.loadPct) ? frame : worst),
    prediction.frames[0] ?? { sample: focus.sample },
  )
  const plausibility = physicsPlausibility({
    envelope,
    predictedLoadPct: Math.abs(peakFrame.sample.loadPct),
    predictedTempC: peakFrame.sample.tempC,
    ratedTempC: focus.profile.ratedTempC,
  })

  const uncertainty = estimateUncertainty({
    currentRisk: prediction.currentRisk,
    peakRisk: prediction.peakRisk,
    etaMinutes: prediction.etaMinutes,
    // The prediction engine reports confidence as a 0–1 fraction; every consumer scales
    // it at the point of display. The uncertainty engine works in 0–100 like the rest of
    // the confidence family, so the conversion happens here rather than being assumed.
    modelConfidence: prediction.confidence * 100,
    dataConfidence: dataQuality.score,
    // Fidelity of the twin covering this asset is resolved by the twin service; here the
    // data score stands in for it, and the two are reconciled at the page level where
    // the twin is actually known.
    twinFidelity: dataQuality.score,
    physicsConfidence: plausibility.score,
    dnaSimilarity: dnaMatches[0]?.similarity ?? 0,
    missingSensors: Math.max(0, dataQuality.sensorsExpected - dataQuality.sensorsReporting),
    untrustedSensors: dataQuality.untrusted.length,
    historicalCoverage: Math.min(100, dataQuality.coverageHours * 4),
    agentConsensus: orchestration.confidence.overall,
  })

  const cascadeTree = buildCascadeTree(cascade)
  const breakpoints = findCascadeBreakpoints(cascadeTree)

  return {
    ts,
    assetCode,
    snapshot,
    graph,
    focus,
    prediction,
    dnaMatches,
    weakSignals,
    cascade,
    actions,
    optimisation,
    rootCause,
    dataQuality,
    agents,
    orchestration,
    resources,
    rulDays,
    actionContext,
    perturbation,
    leadTime,
    envelope,
    physics,
    uncertainty,
    cascadeTree,
    breakpoints,
  }
}

/**
 * The asset the platform would open on its own: highest risk, weighted by how much of
 * the network is behind it, so a critical rural unit does not outrank a strained bulk
 * supply point serving a quarter of a million customers.
 */
export async function pickFocusAsset(options: IntelligenceOptions = {}): Promise<string | null> {
  const [snapshot, graph] = await Promise.all([
    getGridSnapshot({ ts: options.ts, perturbation: options.perturbation }),
    loadGridGraph(),
  ])

  let best: { code: string; score: number } | null = null
  for (const asset of snapshot.assets) {
    const node = graph.byCode.get(asset.profile.code)
    const score = asset.risk.score * (1 + (node?.centrality ?? 0) * 6)
    if (!best || score > best.score) best = { code: asset.profile.code, score }
  }
  return best?.code ?? null
}

/**
 * Three futures, measured rather than asserted (§8).
 *
 * Each branch is a real re-run: the twin is walked forward again with that branch's
 * actions applied, and if it still breaches a limit the cascade is walked from there to
 * count what would go with it. "Assets affected: 0" is a result, not a label.
 */
export async function futureBranches(intel: AssetIntelligence): Promise<FutureBranch[]> {
  const manual = intel.actions.find((action) => action.applied.length > 0)
  const recommended = intel.orchestration.recommended

  const branches: Array<{ key: FutureBranch['key']; label: string; labelAr: string; actions: AppliedAction[] }> = [
    { key: 'no_action', label: 'No action', labelAr: 'بدون إجراء', actions: [] },
    {
      key: 'operator_action',
      label: manual ? manual.title : 'Single operator action',
      labelAr: manual ? manual.titleAr : 'إجراء تشغيلي منفرد',
      actions: manual?.applied ?? [],
    },
    {
      key: 'nabdh_strategy',
      label: recommended ? recommended.label : 'NABDH strategy',
      labelAr: recommended ? recommended.labelAr : 'استراتيجية نَبْض',
      actions: recommended?.actions ?? [],
    },
  ]

  const weights = await loadRiskWeights()
  const region = getRegion(intel.focus.profile.regionCode)

  return Promise.all(
    branches.map(async (branch) => {
      const perturbation: Perturbation = {
        ...intel.perturbation,
        actions: [...(intel.perturbation.actions ?? []), ...branch.actions],
      }
      const envAt = environmentFor(region, perturbation)
      const prediction = predictFailure(intel.focus.profile, intel.ts, envAt, {
        perturbation,
        dnaSimilarity: intel.focus.dnaSimilarity,
        neighbourStress: intel.focus.neighbourStress,
        weights,
        horizonMin: intel.prediction.horizonMin,
        stepMin: 5,
      })

      const breaches = prediction.etaMinutes !== null
      let assetsAffected = 0
      let customersAffected = 0
      let potentialDowntimeMin = 0

      if (breaches) {
        // Only walk the cascade for futures that actually reach a failure: a branch that
        // never breaches has nothing to propagate, and pretending otherwise would put a
        // number on a chain that does not start.
        const snapshot = await getGridSnapshot({
          ts: intel.ts + (prediction.etaMinutes ?? 0) * MS_PER_MIN,
          perturbation,
        })
        const cascade = simulateCascade(
          intel.graph,
          cascadeStates(snapshot.assets),
          intel.assetCode,
          { horizonMin: 60 },
        )
        assetsAffected = cascade.affectedAssets
        customersAffected = cascade.affectedCustomers
        // Restoration time is dominated by how far the chain travelled, not by the
        // origin: reclosing one unit is quick, resynchronising four is not.
        potentialDowntimeMin = Math.round(28 + cascade.maxDepth * 22 + cascade.unservedMw * 0.6)
      }

      const frame = prediction.frames.at(-1)
      return {
        key: branch.key,
        label: branch.label,
        labelAr: branch.labelAr,
        risk: Math.round(prediction.peakRisk * 10) / 10,
        assetsAffected,
        customersAffected,
        potentialDowntimeMin,
        loadPct: Math.round((frame?.sample.loadPct ?? 0) * 10) / 10,
        tempC: Math.round((frame?.sample.tempC ?? 0) * 10) / 10,
        breaches,
        actionCount: branch.actions.length,
      }
    }),
  )
}

/**
 * Persist one orchestration pass: the agent runs, their findings, and the root-cause
 * record. Returns the orchestration id that ties them together.
 *
 * Analysis is recomputed live everywhere it is displayed; these rows exist so a decision
 * taken today can be audited next month against what the agents actually said (§41).
 *
 * Who asked for the pass is recorded by the audit log on the route that called this, so
 * the run rows carry the analysis and the audit trail carries the actor — one fact in
 * one place.
 */
export async function persistOrchestration(intel: AssetIntelligence): Promise<string> {
  const asset = await prisma.asset.findUnique({ where: { code: intel.assetCode }, select: { id: true } })
  const registry = await prisma.aiAgent.findMany({ select: { id: true, key: true } })
  const idByKey = new Map(registry.map((agent) => [agent.key, agent.id]))

  const orchestrationId = `ORC-${intel.ts.toString(36).toUpperCase()}-${intel.assetCode}`
  const startedAt = new Date(intel.ts)

  for (const agent of intel.agents) {
    const agentId = idByKey.get(agent.key)
    if (!agentId) continue
    await prisma.agentRun.create({
      data: {
        code: `${orchestrationId}-${agent.key}`,
        agentId,
        orchestrationId,
        assetId: asset?.id ?? null,
        regionCode: intel.focus.profile.regionCode,
        status: 'completed',
        startedAt,
        finishedAt: new Date(intel.ts + agent.durationMs),
        durationMs: agent.durationMs,
        summary: agent.summary,
        summaryAr: agent.summaryAr,
        confidence: agent.confidence,
        dataQuality: agent.dataQuality,
        inputDigest: agent.inputs.join(' | '),
        findings: {
          create: agent.findings.map((finding, index) => ({
            key: finding.key,
            severity: finding.severity,
            title: finding.title,
            titleAr: finding.titleAr,
            detail: finding.detail,
            detailAr: finding.detailAr,
            metricKey: finding.metricKey,
            metricValue: finding.metricValue,
            metricUnit: finding.metricUnit,
            confidence: finding.confidence,
            ordinal: index,
          })),
        },
      },
    })
  }

  if (intel.rootCause.primary) {
    await prisma.rootCauseAnalysis.create({
      data: {
        code: `RCA-${intel.ts.toString(36).toUpperCase()}-${intel.assetCode}`,
        assetId: asset?.id ?? null,
        assetCode: intel.assetCode,
        primaryCause: intel.rootCause.primary.label,
        primaryCauseAr: intel.rootCause.primary.labelAr,
        confidence: intel.rootCause.confidence,
        summary: intel.rootCause.summary,
        summaryAr: intel.rootCause.summaryAr,
        chainJson: JSON.stringify(intel.rootCause.chain),
        alternatives: intel.rootCause.alternatives.map((entry) => entry.label).join(' | '),
        alternativesAr: intel.rootCause.alternatives.map((entry) => entry.labelAr).join(' | '),
      },
    })
  }

  return orchestrationId
}

/** The seeded agent registry, for the operations room's agent cards. */
export async function listAgents() {
  const rows = await prisma.aiAgent.findMany({ orderBy: { ordinal: 'asc' } })
  if (rows.length > 0) return rows
  // Falling back to the code-level registry keeps the operations room usable on a
  // database that has not been re-seeded since the 2.0 tables were added.
  return AGENT_DEFS.map((def) => ({
    id: def.key,
    key: def.key,
    name: def.name,
    nameAr: def.nameAr,
    role: def.role,
    roleAr: def.roleAr,
    description: def.description,
    descriptionAr: def.descriptionAr,
    ordinal: def.ordinal,
    isActive: true,
    createdAt: new Date(0),
  }))
}

export { REGIONS }
