import 'server-only'

import { prisma } from '../db'
import { getRegion } from '../data/regions'
import { environmentFor, resourcesFor } from '../engine/grid'
import { evaluateStrategy } from '../engine/actions'
import {
  blueTeamCandidates,
  compareRedBlue,
  resourcesUnderStress,
  stressByKind,
  stressLibrary,
  type BlueTeamOutcome,
  type RedBlueComparison,
  type StressKind,
} from '../engine/red-blue'
import { assessResilience } from '../engine/resilience'
import { round } from '../engine/rng'
import type { Perturbation } from '../engine/telemetry'
import { getGridSnapshot, loadRiskWeights } from './grid-service'
import { getResilience } from './resilience-service'

/**
 * Red team / blue team resilience (§24, §25, §26).
 *
 * The service's job is to keep the two halves honest about each other. The red team's
 * scenario removes resources; the blue team is then only offered the resources that
 * survive. Without that, every stress test would look survivable — the response search
 * would cheerfully discharge a battery the scenario had just taken out of service, and
 * the whole exercise would measure nothing.
 *
 * Everything here is evaluated against the model. Nothing is written outside the
 * red/blue tables.
 */

export interface RedBlueRequest {
  stressKind: StressKind
  regionCode?: string | null
  assetCode?: string | null
  ts?: number
  twinCode?: string | null
  userId?: string | null
  persist?: boolean
}

export interface RedBlueView extends RedBlueComparison {
  code: string | null
  scopeKind: string
  scopeCode: string
  assetCode: string
  /** Data confidence lost to the scenario, where it degraded observability (§24). */
  observabilityLossPct: number
  removedResources: string[]
}

export function listStressScenarios() {
  return stressLibrary().map((stress) => ({
    kind: stress.kind,
    title: stress.title,
    titleAr: stress.titleAr,
    description: stress.description,
    descriptionAr: stress.descriptionAr,
    observabilityLossPct: stress.observabilityLossPct,
    removes: stress.removes,
  }))
}

/**
 * Run one stress and search for the best response to it.
 *
 * The three resilience readings — before, during, after — all come from the same scoring
 * function on three different states, so the recovery figure is a measured difference
 * rather than an estimate of one.
 */
export async function runRedBlue(request: RedBlueRequest): Promise<RedBlueView | null> {
  const ts = request.ts ?? Date.now()
  const stress = stressByKind(request.stressKind)

  const [baseSnapshot, weights] = await Promise.all([
    getGridSnapshot({ ts }),
    loadRiskWeights(),
  ])

  const inScope = baseSnapshot.assets.filter((entry) => {
    if (request.assetCode) return entry.profile.code === request.assetCode
    if (request.regionCode) return entry.profile.regionCode === request.regionCode
    return true
  })
  const focus = [...inScope].sort((a, b) => b.risk.score - a.risk.score)[0]
  if (!focus) return null

  const region = getRegion(focus.profile.regionCode)
  const scopeKind = request.assetCode ? 'asset' : request.regionCode ? 'region' : 'national'
  const scopeCode = request.assetCode ?? request.regionCode ?? ''

  // ── Before ────────────────────────────────────────────────────────────────
  // Scope is carried by the perturbation rather than by a separate region argument: the
  // resilience score is a national figure, and a "regional resilience" reading that
  // silently ignored the rest of the grid would be a different metric wearing the same
  // name.
  const before = await getResilience({ ts, includeEnvelope: false })

  // ── During: the red team's conditions ────────────────────────────────────
  const stressed: Perturbation = {
    ...stress.perturbation,
    regionCode: request.regionCode ?? region.code,
  }
  const duringSnapshot = await getGridSnapshot({ ts, perturbation: stressed })
  const duringFocus =
    duringSnapshot.assets.find((entry) => entry.profile.code === focus.profile.code) ?? focus

  const during = await getResilience({ ts, perturbation: stressed, includeEnvelope: false })

  // ── Blue team ─────────────────────────────────────────────────────────────
  const fullResources = resourcesFor(duringFocus.profile, duringSnapshot.assets)
  const available = resourcesUnderStress(
    {
      transferCapacityMw: fullResources.transferCapacityMw,
      batteryPowerMw: fullResources.batteryPowerMw,
      flexibleLoadMw: fullResources.flexibleLoadMw,
      coolingHeadroom: fullResources.coolingHeadroom,
    },
    stress,
  )

  // The deficit is what has to be taken off the asset to bring it back inside its
  // recommended band, not an arbitrary target.
  const deficitMw = Math.max(
    0.5,
    (Math.abs(duringFocus.sample.loadPct) - 88) * 0.01 * duringFocus.profile.capacityMw,
  )

  const envAt = environmentFor(region, stressed)
  const candidates = blueTeamCandidates(duringFocus.profile.code, available, deficitMw)

  const outcomes: BlueTeamOutcome[] = candidates.map((candidate) => {
    const evaluation = evaluateStrategy(
      {
        profile: duringFocus.profile,
        ts,
        envAt,
        perturbation: stressed,
        dnaSimilarity: duringFocus.dnaSimilarity,
        neighbourStress: duringFocus.neighbourStress,
        weights,
        resources: fullResources,
      },
      candidate.actions,
    )

    // Resilience after the response: the same eight-factor score, recomputed with the
    // relieved asset in place. The reserve and condition factors move, which is exactly
    // what a response is supposed to do.
    const relieved = assessResilience({
      ...during.input,
      reserveMarginPct: during.input.reserveMarginPct + evaluation.riskReduction * 0.08,
      meanHealth: Math.min(100, during.input.meanHealth + evaluation.riskReduction * 0.12),
      containedShare: Math.min(1, during.input.containedShare + evaluation.riskReduction * 0.004),
    })

    return {
      candidate,
      resilienceAfter: round(relieved.score, 1),
      riskAfter: round(evaluation.riskAfter, 1),
      // From the response's own profile rather than from the strategy evaluation: the
      // evaluator only knows a duration when the actions came from its own candidate
      // list, and these are built directly from what the scenario left available.
      recoveryMin: candidate.durationMin,
      costIndex: candidate.costIndex,
      customersImpacted: Math.round(
        candidate.actions.some((action) => action.type === 'demand_response')
          ? duringFocus.profile.customersServed * 0.04
          : 0,
      ),
    }
  })

  const comparison = compareRedBlue({
    stress,
    resilienceBefore: before.score,
    resilienceDuring: during.score,
    riskBefore: round(focus.risk.score, 1),
    riskDuring: round(duringFocus.risk.score, 1),
    outcomes,
  })

  let code: string | null = null
  if (request.persist) {
    code = `RB-${Date.now().toString(36).toUpperCase()}`
    const twin = request.twinCode
      ? await prisma.digitalTwin.findUnique({ where: { code: request.twinCode }, select: { id: true } })
      : null

    await prisma.resilienceRedTeamRun.create({
      data: {
        code,
        twinId: twin?.id ?? null,
        scopeKind,
        scopeCode,
        stressKind: stress.kind,
        title: stress.title,
        titleAr: stress.titleAr,
        resilienceBefore: comparison.resilienceBefore,
        resilienceDuring: comparison.resilienceDuring,
        riskBefore: comparison.riskBefore,
        riskDuring: comparison.riskDuring,
        assetsStressed: duringSnapshot.assets.filter((entry) => entry.risk.score >= 60).length,
        weakestFactor: during.weakest?.key ?? '',
        narrative: comparison.narrative,
        narrativeAr: comparison.narrativeAr,
        createdById: request.userId ?? null,
        responses: {
          create: comparison.ranked.map((outcome) => ({
            code: `${code}-B${outcome.rank}`,
            strategyLabel: outcome.candidate.label,
            strategyLabelAr: outcome.candidate.labelAr,
            resilienceAfter: outcome.resilienceAfter,
            riskAfter: outcome.riskAfter,
            recoveryMin: outcome.recoveryMin,
            costIndex: outcome.costIndex,
            customersImpacted: outcome.customersImpacted,
            isRecommended: outcome.isRecommended,
            rank: outcome.rank,
            narrative: `${outcome.candidate.label}: resilience ${comparison.resilienceDuring} → ${outcome.resilienceAfter}`,
            narrativeAr: `${outcome.candidate.labelAr}: المرونة ${comparison.resilienceDuring} ← ${outcome.resilienceAfter}`,
            actions: {
              create: outcome.candidate.actions.map((action) => ({
                actionKey: action.type,
                assetCode: action.assetCode,
                label: outcome.candidate.label,
                labelAr: outcome.candidate.labelAr,
                magnitudeMw: action.magnitudeMw,
              })),
            },
          })),
        },
      },
    })
  }

  return {
    ...comparison,
    code,
    scopeKind,
    scopeCode,
    assetCode: focus.profile.code,
    observabilityLossPct: stress.observabilityLossPct,
    removedResources: stress.removes,
  }
}

export async function listRedBlueRuns(limit = 10) {
  return prisma.resilienceRedTeamRun.findMany({
    orderBy: { createdAt: 'desc' },
    take: limit,
    include: {
      responses: { orderBy: { rank: 'asc' }, include: { actions: true } },
    },
  })
}
