import type { PrismaClient } from '@prisma/client'

import { DEFAULT_CONSTRAINTS } from '../../src/lib/engine/physics'
import { compareToObservation, updateFidelity } from '../../src/lib/engine/verification'
import { sampleAsset, type AssetProfile } from '../../src/lib/engine/telemetry'
import { sampleEnvironment } from '../../src/lib/engine/environment'
import { assessRisk } from '../../src/lib/engine/risk'
import { REGIONS } from '../../src/lib/data/regions'
import { round, seededRange } from '../../src/lib/engine/rng'

/**
 * Demonstration data for the 4.0 assurance surfaces (§96, §97).
 *
 * The tables seeded here are the ones that make the platform's own claims checkable —
 * verification history, decision memory, discovered scenarios — and they have a property
 * the rest of the seed does not: **they must be produced by the engines, not asserted.**
 *
 * A verification row with a hand-written error of "1.5 °C" would make the /verification
 * page a picture of a verification loop rather than a record of one. So every row below
 * is computed: predictions are sampled from the model at one instant, observations are
 * sampled at the horizon, and the errors are whatever the comparison actually produces.
 * When the model changes, these numbers change with it — which is the entire point.
 *
 * Everything is labelled `simulated` except a deliberate minority of decision outcomes,
 * which exist so the platform can demonstrate that it distinguishes the two (§30). Even
 * those are synthetic; the label means "this row is the shape an observed outcome would
 * take", and the UI never presents it as a real prevented failure.
 */

const MIN = 60_000
const HOUR = 60 * MIN
const DAY = 24 * HOUR

interface SeedContext {
  prisma: PrismaClient
  now: number
  profiles: Map<string, AssetProfile>
  userIdByRole: Map<string, string>
  demoAssetCode: string
}

export async function seedAssurance(ctx: SeedContext): Promise<void> {
  await seedPhysicsConstraints(ctx)
  await seedVerifications(ctx)
  await seedDecisionMemory(ctx)
  await seedDiscoveries(ctx)
}

// ─────────────────────────────────────────────────────────────────────────────
// Physics constraints (§10)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The engineering limits, copied from the engine into the database.
 *
 * The engine keeps its own copy as a fallback so the validator can never run with *no*
 * limits, but an operator has to be able to see and change them — a constraint that only
 * exists in compiled code is a constraint nobody can audit.
 */
async function seedPhysicsConstraints({ prisma }: SeedContext): Promise<void> {
  await prisma.physicsConstraint.createMany({
    data: DEFAULT_CONSTRAINTS.map((constraint) => ({
      code: constraint.code,
      kind: constraint.kind,
      scopeKind: constraint.scopeKind,
      scopeCode: constraint.scopeCode,
      name: constraint.label,
      nameAr: constraint.labelAr,
      description: '',
      descriptionAr: '',
      comparator: constraint.comparator,
      limitValue: constraint.limitValue,
      warnValue: constraint.warnValue,
      unit: constraint.unit,
      severity: constraint.severity,
      isActive: true,
      source: 'demo_model',
    })),
  })
}

// ─────────────────────────────────────────────────────────────────────────────
// Verification history (§6, §7)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * A fortnight of closed verifications, plus a few still pending.
 *
 * The pending ones matter as much as the closed ones: a verification page whose backlog
 * is always zero is a page that has quietly stopped counting what it has not checked.
 */
async function seedVerifications(ctx: SeedContext): Promise<void> {
  const { prisma, now, profiles } = ctx

  const twin = await prisma.digitalTwin.findFirst({
    where: { level: 'site' },
    select: { id: true, code: true },
  })
  if (!twin) return

  // Watch-list plant, which is what a verification loop would actually be pointed at.
  const targets = [...profiles.values()]
    .filter((profile) => profile.assetTypeKey === 'transformer')
    .sort((a, b) => a.healthScore - b.healthScore)
    .slice(0, 6)

  const regionByCode = new Map(REGIONS.map((region) => [region.code, region]))
  let fidelity = 82

  for (const profile of targets) {
    const region = regionByCode.get(profile.regionCode)
    if (!region) continue

    // Ten closed verifications per asset. Eight land in the reference window and two in
    // the last two days, because the drift comparison needs *both* windows populated —
    // seeded entirely into the past, it would have nothing recent to compare against and
    // would correctly report that it cannot judge.
    for (let index = 0; index < 10; index += 1) {
      const horizonMin = [15, 30, 60][index % 3]
      const predictedAt =
        index < 8 ? now - (14 - index * 1.4) * DAY : now - (1.6 - (index - 8) * 0.7) * DAY
      const horizonAt = predictedAt + horizonMin * MIN

      const envAtPrediction = sampleEnvironment(region, predictedAt)
      const envAtHorizon = sampleEnvironment(region, horizonAt)

      // The prediction: the model's own forward view, sampled at the horizon instant
      // under the conditions that held when the prediction was made.
      const predictedSample = sampleAsset(profile, horizonAt, envAtPrediction)
      const predictedRisk = assessRisk({
        profile,
        sample: predictedSample,
        env: envAtPrediction,
        dnaSimilarity: 0,
        neighbourStress: 0,
      }).score

      // The observation: the same instant under the conditions that actually arrived.
      const observedSample = sampleAsset(profile, horizonAt, envAtHorizon)
      const observedRisk = assessRisk({
        profile,
        sample: observedSample,
        env: envAtHorizon,
        dnaSimilarity: 0,
        neighbourStress: 0,
      }).score

      // Interval width tracks a plausible confidence for the horizon, so the coverage
      // figure the page computes is a genuine test rather than a guaranteed pass.
      const confidence = round(92 - horizonMin * 0.12 - seededRange(profile.seed, index, 0, 6), 1)
      const spread = (1 - confidence / 100) * 0.8 + 0.03

      const channels = [
        { channel: 'tempC', unit: '°C', predicted: predictedSample.tempC, observed: observedSample.tempC },
        {
          channel: 'loadPct',
          unit: '%',
          predicted: Math.abs(predictedSample.loadPct),
          observed: Math.abs(observedSample.loadPct),
        },
        { channel: 'risk', unit: '%', predicted: predictedRisk, observed: observedRisk },
      ].map((entry) => ({
        ...entry,
        intervalLow: round(entry.predicted * (1 - spread), 3),
        intervalHigh: round(entry.predicted * (1 + spread), 3),
      }))

      const comparison = compareToObservation(
        channels.map((c) => ({
          channel: c.channel,
          unit: c.unit,
          predicted: c.predicted,
          intervalLow: c.intervalLow,
          intervalHigh: c.intervalHigh,
        })),
        channels.map((c) => ({ channel: c.channel, observed: c.observed })),
      )

      const fidelityBefore = fidelity
      fidelity = updateFidelity(fidelity, comparison.meanAbsPctError)

      await prisma.twinVerification.create({
        data: {
          code: `VER-${profile.code}-${index}`,
          twinId: twin.id,
          assetCode: profile.code,
          horizonMin,
          modelVersion: 'Composite Risk Engine 2.4.0',
          twinVersion: twin.code,
          dataSnapshotId: `${profile.code}@${predictedAt}`,
          predictedAt: new Date(predictedAt),
          horizonAt: new Date(horizonAt),
          observedAt: new Date(horizonAt),
          status: 'verified',
          predictedRisk: round(predictedRisk, 2),
          observedRisk: round(observedRisk, 2),
          predictedConfidence: confidence,
          meanAbsError: comparison.meanAbsError,
          worstChannel: comparison.worstChannel,
          worstError: comparison.worstError,
          bias: comparison.bias,
          withinInterval: comparison.withinInterval,
          fidelityBefore,
          fidelityAfter: fidelity,
          channels: {
            create: comparison.channels.map((channel) => ({
              channel: channel.channel,
              unit: channel.unit,
              predicted: channel.predicted,
              observed: channel.observed,
              absError: channel.absError,
              pctError: channel.pctError,
              intervalLow: channel.intervalLow,
              intervalHigh: channel.intervalHigh,
              withinInterval: channel.withinInterval,
            })),
          },
        },
      })
    }
  }

  // Two still open, so the backlog is visible rather than implied.
  for (const profile of targets.slice(0, 2)) {
    const region = regionByCode.get(profile.regionCode)
    if (!region) continue
    const horizonMin = 30
    const predictedAt = now - 10 * MIN
    const horizonAt = predictedAt + horizonMin * MIN
    const env = sampleEnvironment(region, predictedAt)
    const sample = sampleAsset(profile, horizonAt, env)
    const risk = assessRisk({ profile, sample, env, dnaSimilarity: 0, neighbourStress: 0 }).score

    await prisma.twinVerification.create({
      data: {
        code: `VER-${profile.code}-PENDING`,
        twinId: twin.id,
        assetCode: profile.code,
        horizonMin,
        modelVersion: 'Composite Risk Engine 2.4.0',
        twinVersion: twin.code,
        dataSnapshotId: `${profile.code}@${predictedAt}`,
        predictedAt: new Date(predictedAt),
        horizonAt: new Date(horizonAt),
        status: 'pending',
        predictedRisk: round(risk, 2),
        predictedConfidence: 87,
        fidelityBefore: fidelity,
        channels: {
          create: [
            { channel: 'tempC', unit: '°C', predicted: round(sample.tempC, 3), observed: 0, absError: 0, pctError: 0, withinInterval: false },
            { channel: 'loadPct', unit: '%', predicted: round(Math.abs(sample.loadPct), 3), observed: 0, absError: 0, pctError: 0, withinInterval: false },
            { channel: 'risk', unit: '%', predicted: round(risk, 3), observed: 0, absError: 0, pctError: 0, withinInterval: false },
          ],
        },
      },
    })
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Decision memory (§27–§31)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * A history of decisions, deliberately shaped so the learning engine has something to
 * find: the same strategy repeatedly chosen for thermal stress, and one DNA pattern whose
 * events kept occurring despite a recommendation.
 *
 * The situations are sampled from the model at past instants rather than invented, so a
 * similarity match against today's state is a genuine comparison of two real states.
 */
async function seedDecisionMemory(ctx: SeedContext): Promise<void> {
  const { prisma, now, profiles, userIdByRole, demoAssetCode } = ctx
  const supervisorId = userIdByRole.get('control_operator') ?? null
  const regionByCode = new Map(REGIONS.map((region) => [region.code, region]))

  const candidates = [...profiles.values()]
    .filter((profile) => profile.assetTypeKey === 'transformer')
    .sort((a, b) => a.healthScore - b.healthScore)
    .slice(0, 9)

  // The demo asset first, so the similarity search on /ai-operations has close matches
  // for the situation a presenter will actually be looking at.
  const demo = profiles.get(demoAssetCode)
  const ordered = demo ? [demo, ...candidates.filter((p) => p.code !== demoAssetCode)] : candidates

  const STRATEGIES = [
    {
      key: 'transfer_storage',
      label: 'Load transfer + storage',
      labelAr: 'نقل الحمل + التخزين',
      rootCause: 'thermal_stress',
      dna: 'DNA-387',
    },
    {
      key: 'transfer_storage',
      label: 'Load transfer + storage',
      labelAr: 'نقل الحمل + التخزين',
      rootCause: 'thermal_stress',
      dna: 'DNA-387',
    },
    {
      key: 'demand_response',
      label: 'Flexible demand deferral',
      labelAr: 'تأجيل الطلب المرن',
      rootCause: 'demand_surge',
      dna: 'DNA-204',
    },
  ]

  for (const [index, profile] of ordered.entries()) {
    const region = regionByCode.get(profile.regionCode)
    if (!region) continue

    const strategy = STRATEGIES[index % STRATEGIES.length]
    const at = now - (60 - index * 5) * DAY
    const env = sampleEnvironment(region, at)
    const sample = sampleAsset(profile, at, env)
    const risk = assessRisk({ profile, sample, env, dnaSimilarity: 78, neighbourStress: 0.2 }).score

    // Outcomes are mostly prevented, with a deliberate minority that occurred anyway —
    // otherwise the learning engine would only ever find "everything works", which is
    // both useless and untrue.
    const occurred = index % 4 === 3
    const observed = index % 3 === 0
    const riskAfter = occurred ? round(risk * 0.72, 1) : round(risk * 0.11, 1)
    const at2 = new Date(at)

    const memory = await prisma.decisionMemory.create({
      data: {
        code: `DM-SEED-${index + 1}`,
        assetCode: profile.code,
        regionCode: profile.regionCode,
        assetTypeKey: profile.assetTypeKey,
        title: `${strategy.label} on ${profile.code}`,
        titleAr: `${strategy.labelAr} على ${profile.code}`,
        loadPct: round(Math.abs(sample.loadPct), 1),
        tempC: round(sample.tempC, 1),
        ambientC: round(sample.ambientC, 1),
        healthScore: round(profile.healthScore, 1),
        riskBefore: round(risk, 1),
        hourOfDay: at2.getUTCHours(),
        monthOfYear: at2.getUTCMonth() + 1,
        dnaPatternCode: strategy.dna,
        dnaSimilarity: round(74 + seededRange(profile.seed, 11, 0, 16), 1),
        rootCauseKey: strategy.rootCause,
        cascadeDepth: index % 3,
        strategyKey: strategy.key,
        strategyLabel: strategy.label,
        strategyLabelAr: strategy.labelAr,
        riskAfter,
        approverId: supervisorId,
        approvedAt: at2,
        evidenceKind: observed ? 'observed' : 'simulated',
        outcomeStatus: occurred ? 'occurred' : 'prevented',
        createdAt: at2,
      },
    })

    await prisma.decisionOutcome.create({
      data: {
        memoryId: memory.id,
        result: occurred ? 'occurred' : 'prevented',
        observedRisk: riskAfter,
        predictedRisk: riskAfter,
        deltaRisk: 0,
        observedAt: new Date(at + 3 * HOUR),
        evidenceKind: observed ? 'observed' : 'simulated',
        notes: occurred
          ? 'The intervention was issued but the asset crossed its limit anyway.'
          : 'The asset stayed inside its envelope through the peak window.',
        notesAr: occurred
          ? 'صدر التدخل لكن الأصل تجاوز حده رغم ذلك.'
          : 'بقي الأصل داخل مظروفه طوال نافذة الذروة.',
        recordedById: supervisorId,
        createdAt: new Date(at + 3 * HOUR),
      },
    })

    if (index % 3 === 0) {
      await prisma.decisionLesson.create({
        data: {
          memoryId: memory.id,
          text: 'Storage was sized to the deficit rather than to the battery, which kept the reserve intact for the following hour.',
          textAr: 'حُدد حجم التخزين وفق العجز لا وفق البطارية، ما أبقى الاحتياطي سليمًا للساعة التالية.',
          authorId: supervisorId,
          isApproved: true,
          createdAt: new Date(at + 4 * HOUR),
        },
      })
    }
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Discovered scenarios (§21)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Two findings from a previous search, so the Discovery Lab opens with something to read
 * and the novelty score on a fresh run has something to be novel against.
 */
async function seedDiscoveries(ctx: SeedContext): Promise<void> {
  const { prisma, now, profiles, demoAssetCode } = ctx
  const profile = profiles.get(demoAssetCode)
  if (!profile) return

  const region = REGIONS.find((entry) => entry.code === profile.regionCode)
  if (!region) return

  const at = now - 3 * DAY

  await prisma.discoveryRun.create({
    data: {
      code: 'DISC-SEED-1',
      objective: 'hidden_risk',
      scopeKind: 'region',
      scopeCode: profile.regionCode,
      seed: 20260829,
      maxSimulations: 60,
      simulationsRun: 58,
      maxDurationMs: 20_000,
      durationMs: 4_180,
      status: 'completed',
      discovered: 2,
      summary: '2 findings from 58 simulations. Strongest: ambient +6 °C with demand +8 % takes risk to 88 % at ' + demoAssetCode,
      summaryAr: `نتيجتان من ٥٨ محاكاة. الأقوى: ارتفاع الحرارة ٦° مع زيادة الطلب ٨٪ يرفع الخطر إلى ٨٨٪ عند ${demoAssetCode}`,
      createdAt: new Date(at),
      completedAt: new Date(at + 4_180),
      discoveries: {
        create: [
          {
            code: 'SD-SEED-1',
            title: `Ambient +6 °C with demand +8 % takes risk to 88 % at ${demoAssetCode}`,
            titleAr: `ارتفاع الحرارة ٦° مع زيادة الطلب ٨٪ يرفع الخطر إلى ٨٨٪ عند ${demoAssetCode}`,
            originAsset: demoAssetCode,
            regionCode: profile.regionCode,
            riskBefore: 41.2,
            riskAfter: 88.4,
            cascadeProbability: 0.62,
            cascadeDepth: 2,
            assetsAffected: 4,
            noveltyScore: 78,
            riskScore: 71.5,
            probability: 84.3,
            severity: 74.1,
            reproducibility: 96,
            confidence: 79.8,
            status: 'new',
            narrative: `Ambient temperature +6 °C with Demand +8 % takes risk to 88 % at ${demoAssetCode}`,
            narrativeAr: `الحرارة المحيطة +٦° مع الطلب +٨٪ يرفعان الخطر إلى ٨٨٪ عند ${demoAssetCode}`,
            createdAt: new Date(at),
            conditions: {
              create: [
                { leverKey: 'temperature', label: 'Ambient temperature', labelAr: 'الحرارة المحيطة', magnitude: 6, unit: '°C', plausibility: 100 },
                { leverKey: 'demand', label: 'Demand', labelAr: 'الطلب', magnitude: 8, unit: '%', plausibility: 100 },
              ],
            },
          },
          {
            code: 'SD-SEED-2',
            title: 'Storage unavailable with solar −20 % during the evening ramp',
            titleAr: 'تعذّر التخزين مع انخفاض الطاقة الشمسية ٢٠٪ أثناء صعود المساء',
            originAsset: demoAssetCode,
            regionCode: profile.regionCode,
            riskBefore: 41.2,
            riskAfter: 76.9,
            cascadeProbability: 0.34,
            cascadeDepth: 1,
            assetsAffected: 2,
            noveltyScore: 64,
            riskScore: 58.2,
            probability: 61.7,
            severity: 61.4,
            reproducibility: 92,
            confidence: 62.1,
            status: 'new',
            narrative: 'Storage unavailable with Solar output −20 % takes risk to 77 %',
            narrativeAr: 'تعذّر التخزين مع انخفاض الإنتاج الشمسي ٢٠٪ يرفع الخطر إلى ٧٧٪',
            createdAt: new Date(at),
            conditions: {
              create: [
                { leverKey: 'battery', label: 'Storage unavailable', labelAr: 'تعذّر توفر التخزين', magnitude: 1, unit: '', plausibility: 62 },
                { leverKey: 'solar', label: 'Solar output', labelAr: 'إنتاج الطاقة الشمسية', magnitude: -20, unit: '%', plausibility: 74 },
              ],
            },
          },
        ],
      },
    },
  })
}

/**
 * Clear the 4.0 tables.
 *
 * Child-first, because these tables carry cascade relations and a parent-first delete
 * would fail on the first foreign key it met.
 */
export async function resetAssurance(prisma: PrismaClient): Promise<void> {
  await prisma.twinPredictionError.deleteMany()
  await prisma.twinVerification.deleteMany()
  await prisma.verificationMetric.deleteMany()

  await prisma.physicsCheck.deleteMany()
  await prisma.physicsValidationRun.deleteMany()
  await prisma.physicsConstraint.deleteMany()
  await prisma.assetOperatingEnvelope.deleteMany()
  await prisma.uncertaintyEstimate.deleteMany()

  await prisma.discoveryCondition.deleteMany()
  await prisma.scenarioDiscovery.deleteMany()
  await prisma.discoveryRun.deleteMany()
  await prisma.adversarialTest.deleteMany()

  await prisma.blueTeamAction.deleteMany()
  await prisma.resilienceBlueTeamRun.deleteMany()
  await prisma.resilienceRedTeamRun.deleteMany()

  await prisma.decisionLesson.deleteMany()
  await prisma.decisionOutcome.deleteMany()
  await prisma.decisionMemory.deleteMany()
  await prisma.learningCandidate.deleteMany()

  await prisma.strategyTradeoff.deleteMany()
  await prisma.paretoStrategy.deleteMany()
  await prisma.cascadeProbability.deleteMany()
  await prisma.cascadeBreakpoint.deleteMany()

  await prisma.evidenceEdge.deleteMany()
  await prisma.evidenceNode.deleteMany()

  await prisma.modelDriftEvent.deleteMany()
  await prisma.dataProvenance.deleteMany()
  await prisma.approvalRecord.deleteMany()
  await prisma.computeJob.deleteMany()
}
