import 'server-only'

import { prisma } from '../db'
import { getRegion } from '../data/regions'
import { environmentFor } from '../engine/grid'
import { predictFailure } from '../engine/prediction'
import { simulateCascade } from '../engine/cascade'
import {
  DISCOVERY_LEVERS,
  discoverScenarios,
  fingerprint,

  type DiscoveryObjective,
  type DiscoveryRunResult,
} from '../engine/discovery'
import type { Perturbation } from '../engine/telemetry'
import { round } from '../engine/rng'
import { getGridSnapshot, loadRiskWeights } from './grid-service'
import { cascadeStates, loadGridGraph } from './graph-service'
import { runAdversarial, type AdversarialView } from './resilience-service'

/**
 * Autonomous scenario discovery, run against the sandbox (§18–§23).
 *
 * Two guarantees this service is responsible for, neither of which belongs in the engine:
 *
 *   1. **Isolation.** Every simulation here is a pure evaluation of the grid model under
 *      hypothetical conditions. Nothing in this file writes telemetry, touches an asset
 *      row, or produces an alert. The only tables it writes are the discovery tables,
 *      which is the structural version of §87 rather than a promise to be careful.
 *
 *   2. **Budget.** The search is bounded in simulations *and* in wall-clock, and both
 *      bounds are recorded on the run. A demo server that disappears into a
 *      combinatorial search is a demo that ends early (§94).
 */

const DEFAULT_MAX_SIMULATIONS = 60
const DEFAULT_BUDGET_MS = 20_000

export interface DiscoveryRequest {
  objective: DiscoveryObjective
  regionCode?: string | null
  assetCode?: string | null
  ts?: number
  seed?: number
  maxSimulations?: number
  maxResults?: number
  userId?: string | null
  persist?: boolean
}

export interface DiscoveryOutcome extends DiscoveryRunResult {
  runCode: string | null
  durationMs: number
  scopeKind: string
  scopeCode: string
  discoveryCodes: string[]
}

/**
 * Run a bounded search and, optionally, keep what it found.
 *
 * The evaluator is built once and closes over the base snapshot, so the 60 simulations a
 * run performs are 60 forward walks rather than 60 database round-trips.
 */
export async function runDiscovery(request: DiscoveryRequest): Promise<DiscoveryOutcome> {
  const startedAt = Date.now()
  const ts = request.ts ?? Date.now()
  const maxSimulations = Math.min(request.maxSimulations ?? DEFAULT_MAX_SIMULATIONS, 200)

  const [graph, weights, known] = await Promise.all([
    loadGridGraph(),
    loadRiskWeights(),
    knownFingerprints(),
  ])

  // The focus asset: the one already under most pressure in scope, because a discovery
  // about the strongest unit on the grid is not a discovery.
  const baseSnapshot = await getGridSnapshot({ ts })
  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 focusEntry = [...inScope].sort((a, b) => b.risk.score - a.risk.score)[0]
  if (!focusEntry) {
    return {
      objective: request.objective,
      discoveries: [],
      simulationsRun: 0,
      budgetExhausted: false,
      skippedImplausible: 0,
      summary: 'No asset in scope.',
      summaryAr: 'لا يوجد أصل ضمن النطاق.',
      runCode: null,
      durationMs: 0,
      scopeKind: request.assetCode ? 'asset' : request.regionCode ? 'region' : 'national',
      scopeCode: request.assetCode ?? request.regionCode ?? '',
      discoveryCodes: [],
    }
  }

  const region = getRegion(focusEntry.profile.regionCode)
  const scopeRegion = request.regionCode ?? region.code
  const baseStates = cascadeStates(baseSnapshot.assets)

  const evaluate = (perturbation: Perturbation) => {
    // Time is checked inside the evaluator rather than around the loop, so a run that
    // overruns stops at the next simulation rather than at the next objective.
    if (Date.now() - startedAt > DEFAULT_BUDGET_MS) return null

    const scoped: Perturbation = { ...perturbation, regionCode: scopeRegion }
    const envAt = environmentFor(region, scoped)

    const prediction = predictFailure(focusEntry.profile, ts, envAt, {
      perturbation: scoped,
      dnaSimilarity: focusEntry.dnaSimilarity,
      neighbourStress: focusEntry.neighbourStress,
      weights,
      horizonMin: 120,
      stepMin: 15,
    })

    return {
      riskBefore: round(focusEntry.risk.score, 1),
      riskAfter: round(prediction.peakRisk, 1),
      // The cascade walk is the expensive half, so it only runs when the conditions
      // actually took the asset somewhere interesting.
      ...(prediction.peakRisk >= 70
        ? cascadeFor(graph, baseStates, focusEntry.profile.code)
        : { cascadeProbability: 0, cascadeDepth: 0, assetsAffected: 1 }),
      originAsset: focusEntry.profile.code,
      regionCode: scopeRegion,
    }
  }

  const result = discoverScenarios(evaluate, {
    objective: request.objective,
    seed: request.seed ?? 20260829,
    maxSimulations,
    maxResults: request.maxResults ?? 6,
    known,
    levers: DISCOVERY_LEVERS,
  })

  const durationMs = Date.now() - startedAt
  const scopeKind = request.assetCode ? 'asset' : request.regionCode ? 'region' : 'national'
  const scopeCode = request.assetCode ?? request.regionCode ?? ''

  if (!request.persist) {
    return { ...result, runCode: null, durationMs, scopeKind, scopeCode, discoveryCodes: [] }
  }

  const runCode = `DISC-${Date.now().toString(36).toUpperCase()}`
  const discoveryCodes: string[] = []

  await prisma.discoveryRun.create({
    data: {
      code: runCode,
      objective: request.objective,
      scopeKind,
      scopeCode,
      seed: request.seed ?? 20260829,
      maxSimulations,
      simulationsRun: result.simulationsRun,
      maxDurationMs: DEFAULT_BUDGET_MS,
      durationMs,
      status: 'completed',
      discovered: result.discoveries.length,
      summary: result.summary,
      summaryAr: result.summaryAr,
      startedById: request.userId ?? null,
      completedAt: new Date(),
      discoveries: {
        create: result.discoveries.map((discovery, index) => {
          const code = `SD-${runCode.slice(5)}-${index + 1}`
          discoveryCodes.push(code)
          return {
            code,
            title: discovery.narrative.slice(0, 160),
            titleAr: discovery.narrativeAr.slice(0, 160),
            originAsset: discovery.evaluation.originAsset,
            regionCode: discovery.evaluation.regionCode,
            riskBefore: discovery.evaluation.riskBefore,
            riskAfter: discovery.evaluation.riskAfter,
            cascadeProbability: discovery.evaluation.cascadeProbability,
            cascadeDepth: discovery.evaluation.cascadeDepth,
            assetsAffected: discovery.evaluation.assetsAffected,
            noveltyScore: discovery.noveltyScore,
            riskScore: discovery.riskScore,
            probability: discovery.probability,
            severity: discovery.severity,
            reproducibility: discovery.reproducibility,
            confidence: discovery.confidence,
            status: 'new',
            narrative: discovery.narrative,
            narrativeAr: discovery.narrativeAr,
            conditions: {
              create: discovery.conditions.map((condition) => ({
                leverKey: condition.leverKey,
                label: condition.label,
                labelAr: condition.labelAr,
                magnitude: condition.magnitude,
                unit: condition.unit,
                plausibility: condition.plausibility,
              })),
            },
          }
        }),
      },
    },
  })

  return { ...result, runCode, durationMs, scopeKind, scopeCode, discoveryCodes }
}

/**
 * Walk the cascade for one candidate.
 *
 * Synchronous by design: the caller already holds the graph and a set of asset states, so
 * this stays inside the evaluator rather than turning each of sixty candidates into a
 * database round-trip.
 */
function cascadeFor(
  graph: Awaited<ReturnType<typeof loadGridGraph>>,
  states: ReturnType<typeof cascadeStates>,
  originCode: string,
) {
  const cascade = simulateCascade(graph, states, originCode, { horizonMin: 60 })
  return {
    // The first propagating hop is the probability that the chain leaves the origin at
    // all, which is the number that makes a cascade-trigger finding interesting.
    cascadeProbability: cascade.steps.length > 1 ? cascade.steps[1].probability : 0,
    cascadeDepth: cascade.maxDepth,
    assetsAffected: cascade.affectedAssets,
  }
}

/** Fingerprints of everything already discovered, so novelty means something. */
async function knownFingerprints(): Promise<string[]> {
  const rows = await prisma.scenarioDiscovery.findMany({
    include: { conditions: true },
    take: 200,
    orderBy: { createdAt: 'desc' },
  })
  return rows.map((row) =>
    fingerprint(
      row.conditions.map((c) => ({
        leverKey: c.leverKey,
        label: c.label,
        labelAr: c.labelAr,
        magnitude: c.magnitude,
        unit: c.unit,
        plausibility: c.plausibility,
      })),
    ),
  )
}

export interface DiscoveryListItem {
  code: string
  runCode: string
  objective: string
  title: string
  titleAr: string
  narrative: string
  narrativeAr: string
  originAsset: string
  regionCode: string
  riskBefore: number
  riskAfter: number
  cascadeProbability: number
  cascadeDepth: number
  assetsAffected: number
  noveltyScore: number
  riskScore: number
  probability: number
  severity: number
  reproducibility: number
  confidence: number
  status: string
  createdAt: number
  conditions: Array<{
    leverKey: string
    label: string
    labelAr: string
    magnitude: number
    unit: string
    plausibility: number
  }>
}

export async function listDiscoveries(
  options: { limit?: number; status?: string } = {},
): Promise<DiscoveryListItem[]> {
  const rows = await prisma.scenarioDiscovery.findMany({
    where: options.status ? { status: options.status } : undefined,
    include: { conditions: true, run: { select: { code: true, objective: true } } },
    orderBy: [{ riskScore: 'desc' }, { createdAt: 'desc' }],
    take: options.limit ?? 20,
  })

  return rows.map((row) => ({
    code: row.code,
    runCode: row.run.code,
    objective: row.run.objective,
    title: row.title,
    titleAr: row.titleAr,
    narrative: row.narrative,
    narrativeAr: row.narrativeAr,
    originAsset: row.originAsset,
    regionCode: row.regionCode,
    riskBefore: row.riskBefore,
    riskAfter: row.riskAfter,
    cascadeProbability: row.cascadeProbability,
    cascadeDepth: row.cascadeDepth,
    assetsAffected: row.assetsAffected,
    noveltyScore: row.noveltyScore,
    riskScore: row.riskScore,
    probability: row.probability,
    severity: row.severity,
    reproducibility: row.reproducibility,
    confidence: row.confidence,
    status: row.status,
    createdAt: row.createdAt.getTime(),
    conditions: row.conditions.map((c) => ({
      leverKey: c.leverKey,
      label: c.label,
      labelAr: c.labelAr,
      magnitude: c.magnitude,
      unit: c.unit,
      plausibility: c.plausibility,
    })),
  }))
}

export async function listDiscoveryRuns(limit = 10) {
  return prisma.discoveryRun.findMany({
    orderBy: { createdAt: 'desc' },
    take: limit,
    select: {
      code: true,
      objective: true,
      scopeKind: true,
      scopeCode: true,
      simulationsRun: true,
      maxSimulations: true,
      durationMs: true,
      discovered: true,
      summary: true,
      summaryAr: true,
      status: true,
      createdAt: true,
    },
  })
}

// ─────────────────────────────────────────────────────────────────────────────
// The adversarial twin (§23, §107)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * "What breaks this asset first, using the least unusual conditions?"
 *
 * The search itself already exists in the resilience service, where it was built for the
 * 3.0 resilience lab; this is the 4.0 half that keeps a record of it. Re-implementing the
 * search here would give the Discovery Lab and the Resilience Lab two subtly different
 * answers to the same question, which is worse than either answer alone.
 *
 * Only `adversarial_tests` is written. The `sandboxOnly` column records a property the
 * code structurally has — nothing in this path can reach a live table — rather than an
 * assertion about intent (§87).
 */
export interface AdversarialOutcome extends AdversarialView {
  code: string | null
}

export async function runAdversarialTest(input: {
  assetCode: string
  ts?: number
  targetRisk?: number
  goal?: string
  twinCode?: string | null
  userId?: string | null
  persist?: boolean
}): Promise<AdversarialOutcome | null> {
  const view = await runAdversarial(input.assetCode, {
    ts: input.ts,
    targetRisk: input.targetRisk,
  })
  if (!view) return null

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

    await prisma.adversarialTest.create({
      data: {
        code,
        twinId: twin?.id ?? null,
        assetCode: input.assetCode,
        goal: input.goal ?? 'critical',
        sandboxOnly: true,
        found: view.found,
        iterations: view.evaluations,
        riskBefore: round(view.riskBefore, 1),
        riskAfter: round(view.metric, 1),
        // Reported as a 0–100 ordinariness score rather than a raw cost, so a higher
        // number always means "more likely to actually happen" the way it does everywhere
        // else in the product.
        plausibility: round(Math.max(0, 100 - view.plausibilityCost), 1),
        weakestAsset: input.assetCode,
        summary: view.summary,
        summaryAr: view.summaryAr,
        createdById: input.userId ?? null,
      },
    })
  }

  return { ...view, code }
}

export async function listAdversarialTests(limit = 10) {
  return prisma.adversarialTest.findMany({
    orderBy: { createdAt: 'desc' },
    take: limit,
    select: {
      code: true,
      assetCode: true,
      goal: true,
      found: true,
      iterations: true,
      riskBefore: true,
      riskAfter: true,
      plausibility: true,
      summary: true,
      summaryAr: true,
      sandboxOnly: true,
      createdAt: true,
    },
  })
}
