import 'server-only'

import { prisma } from '../db'
import { buildEvidenceBundle, type EvidenceBundle } from '../engine/evidence'
import { assessImpact, type ImpactAssessment } from '../engine/impact'
import { round } from '../engine/rng'
import type { DataSource } from '../domain/provenance'
import { analyseAsset, type AssetIntelligence } from './intelligence-service'
import { getTwinState } from './twin-service'
import type { Perturbation } from '../engine/telemetry'

/**
 * Evidence Mode and the PROVE IT button (§50, §51, §52, §53).
 *
 * The rule this service follows is that the bundle is assembled from artefacts the
 * analysis actually produced. Where a stage did not run, no node is emitted and the gap
 * is reported — a chain with a hole in it must look like a chain with a hole in it, not
 * like a complete one.
 *
 * The bundle is persisted so that a decision can be produced as evidence later, after the
 * live numbers have moved on. That is the difference between explainability and a
 * screenshot.
 */

export interface EvidenceRequest {
  assetCode: string
  ts?: number
  perturbation?: Perturbation
  twinCode?: string | null
  persist?: boolean
}

export interface EvidenceView {
  bundle: EvidenceBundle
  impact: ImpactAssessment
  /** Everything a judge needs to check the chain is reproducible (§53). */
  reproducibility: {
    modelVersion: string
    twinVersion: string
    dataSnapshotId: string
    ts: number
    perturbation: Perturbation
    seedNote: string
  }
}

/**
 * Build the evidence chain for one asset.
 *
 * Reuses `analyseAsset` rather than re-deriving anything: the point of Evidence Mode is
 * to show the path the console took, and a second, parallel derivation would be a
 * different path that merely resembles it.
 */
export async function buildEvidence(request: EvidenceRequest): Promise<EvidenceView | null> {
  const ts = request.ts ?? Date.now()
  const intel = await analyseAsset(request.assetCode, {
    ts,
    perturbation: request.perturbation,
    includeLeadTime: true,
  })
  if (!intel) return null

  const twinState = request.twinCode ? await getTwinState(request.twinCode, { ts }) : null
  const modelVersion = await activeModelVersion()

  const code = `EV-${Date.now().toString(36).toUpperCase()}-${request.assetCode}`
  const impact = impactFor(intel)

  // Assembled once. The impact figures are computed first so the node on the chain
  // carries the same numbers the impact panel shows — building the bundle twice, once
  // without impact and once with, would let the two drift apart the first time either
  // side was edited.
  const bundle = buildEvidenceBundle({
    code,
    assetCode: intel.assetCode,
    modelVersion,
    twinVersion: twinState?.twin.code ?? '',
    observations: observationsFrom(intel),
    dataQuality: {
      score: round(intel.dataQuality.score, 1),
      band: intel.dataQuality.band,
      missingSensors: Math.max(
        0,
        intel.dataQuality.sensorsExpected - intel.dataQuality.sensorsReporting,
      ),
      untrusted: intel.dataQuality.untrusted.length,
    },
    dna: intel.dnaMatches[0]
      ? {
          code: intel.dnaMatches[0].pattern.code,
          name: intel.dnaMatches[0].pattern.name,
          nameAr: intel.dnaMatches[0].pattern.nameAr,
          similarity: intel.dnaMatches[0].similarity,
        }
      : null,
    prediction: {
      currentRisk: intel.prediction.currentRisk,
      peakRisk: intel.prediction.peakRisk,
      etaMinutes: intel.prediction.etaMinutes,
      confidence: round(intel.prediction.confidence * 100, 1),
    },
    uncertainty: {
      probability: intel.uncertainty.probability,
      windowLowMinutes: intel.uncertainty.windowLowMinutes,
      windowHighMinutes: intel.uncertainty.windowHighMinutes,
      predictionConfidence: intel.uncertainty.predictionConfidence,
    },
    physics: {
      verdict: intel.physics.verdict,
      checksRun: intel.physics.checks.length,
      checksFailed: intel.physics.failed.length,
      confidence: intel.physics.confidence,
      summary: intel.physics.summary,
      summaryAr: intel.physics.summaryAr,
    },
    twin: twinState
      ? {
          code: twinState.twin.code,
          fidelity: round(twinState.fidelity.score, 1),
          band: twinState.fidelity.band,
        }
      : null,
    cascade: {
      steps: intel.cascade.steps.length,
      affectedAssets: intel.cascade.affectedAssets,
      probability: intel.cascade.steps[1]?.probability ?? 0,
      containedAtMin: intel.cascade.containedAtMin,
      maxDepth: intel.cascade.maxDepth,
    },
    simulation: intel.orchestration.recommended
      ? {
          code: `sim:${intel.orchestration.recommended.key}`,
          riskBefore: intel.orchestration.recommended.evaluation.riskBefore,
          riskAfter: intel.orchestration.recommended.evaluation.riskAfter,
          strategyLabel: intel.orchestration.recommended.label,
          strategyLabelAr: intel.orchestration.recommended.labelAr,
        }
      : null,
    recommendation: intel.orchestration.recommended
      ? {
          label: intel.orchestration.recommended.label,
          labelAr: intel.orchestration.recommended.labelAr,
          confidence: intel.orchestration.confidence.recommendation,
        }
      : null,
    impact: {
      customersProtected: Math.round(
        impact.customer.find((f) => f.key === 'customers_protected')?.value ?? 0,
      ),
      unservedAvoidedMw: impact.technical.find((f) => f.key === 'capacity_protected')?.value ?? 0,
      confidence: impact.customer[0]?.confidence ?? 0,
    },
  })

  if (request.persist) await persistBundle(bundle)

  return {
    bundle,
    impact,
    reproducibility: {
      modelVersion,
      twinVersion: twinState?.twin.code ?? '',
      dataSnapshotId: `${intel.assetCode}@${intel.ts}`,
      ts: intel.ts,
      perturbation: intel.perturbation,
      seedNote:
        'The simulation engine is deterministic: the same timestamp, asset and scenario reproduce this chain exactly.',
    },
  }
}

/** The channels that actually fed the analysis, with their provenance. */
function observationsFrom(intel: AssetIntelligence) {
  const trustByKind = new Map(intel.dataQuality.sensors.map((sensor) => [sensor.kind, sensor.score]))
  const source: DataSource = 'synthetic'

  return [
    {
      channel: 'tempC',
      label: 'Top-oil temperature',
      labelAr: 'حرارة الزيت العلوي',
      value: intel.focus.sample.tempC,
      unit: '°C',
      sourceType: source,
      sourceId: `${intel.assetCode}-TEMP`,
      freshnessMin: 0,
      trust: trustByKind.get('temperature') ?? 80,
    },
    {
      channel: 'loadPct',
      label: 'Loading',
      labelAr: 'التحميل',
      value: Math.abs(intel.focus.sample.loadPct),
      unit: '%',
      sourceType: source,
      sourceId: `${intel.assetCode}-LOAD`,
      freshnessMin: 0,
      trust: trustByKind.get('load') ?? 80,
    },
    {
      channel: 'voltageKv',
      label: 'Voltage',
      labelAr: 'الجهد',
      value: intel.focus.sample.voltageKv,
      unit: 'kV',
      sourceType: source,
      sourceId: `${intel.assetCode}-VOLT`,
      freshnessMin: 0,
      trust: trustByKind.get('voltage') ?? 80,
    },
    {
      channel: 'currentA',
      label: 'Current',
      labelAr: 'التيار',
      value: intel.focus.sample.currentA,
      unit: 'A',
      sourceType: source,
      sourceId: `${intel.assetCode}-CURR`,
      freshnessMin: 0,
      trust: trustByKind.get('current') ?? 80,
    },
  ]
}

/** The impact assessment for one analysis (§54–§57). */
export function impactFor(intel: AssetIntelligence): ImpactAssessment {
  const recommended = intel.orchestration.recommended
  const riskBefore = recommended?.evaluation.riskBefore ?? intel.orchestration.unifiedRisk
  const riskAfter = recommended?.evaluation.riskAfter ?? riskBefore

  return assessImpact({
    riskBefore,
    riskAfter,
    customersAtRisk: intel.cascade.affectedCustomers || intel.focus.profile.customersServed,
    unservedMw: intel.cascade.unservedMw || Math.abs(intel.focus.sample.loadMw) * 0.3,
    assetsAffected: intel.cascade.affectedAssets,
    leadMinutes: intel.leadTime?.leadMinutes ?? null,
    loadPctBefore: Math.abs(intel.focus.sample.loadPct),
    loadPctAfter: recommended?.evaluation.loadAfterPct ?? Math.abs(intel.focus.sample.loadPct),
    tempCBefore: intel.focus.sample.tempC,
    tempCAfter: recommended?.evaluation.tempAfterC ?? intel.focus.sample.tempC,
    rulDaysBefore: intel.rulDays,
    // A relieved asset ages more slowly. Scaled by how much of the risk was removed
    // rather than asserted, and reported as projected.
    rulDaysAfter:
      intel.rulDays *
      (1 + (riskBefore > 0 ? Math.max(0, riskBefore - riskAfter) / riskBefore : 0) * 1.4),
    executionMinutes: recommended?.executionMin ?? 20,
    predictionConfidence: intel.uncertainty.predictionConfidence,
    twinFidelity: intel.uncertainty.twinFidelity,
    evidenceKind: 'simulated',
  })
}

async function activeModelVersion(): Promise<string> {
  const model = await prisma.modelVersion.findFirst({
    where: { status: 'active' },
    orderBy: { trainedAt: 'desc' },
    select: { version: true, name: true },
  })
  return model ? `${model.name} ${model.version}` : 'nabdh-engine'
}

async function persistBundle(bundle: EvidenceBundle): Promise<void> {
  await prisma.$transaction([
    prisma.evidenceNode.createMany({
      data: bundle.nodes.map((node) => ({
        bundleCode: bundle.code,
        nodeKey: node.key,
        stage: node.stage,
        title: node.title,
        titleAr: node.titleAr,
        detail: node.detail,
        detailAr: node.detailAr,
        sourceType: node.sourceType,
        sourceId: node.sourceId,
        value: node.value,
        unit: node.unit,
        confidence: node.confidence,
        freshnessMin: node.freshnessMin,
        modelVersion: node.modelVersion,
        orderIndex: node.orderIndex,
      })),
    }),
    prisma.evidenceEdge.createMany({
      data: bundle.edges.map((edge) => ({
        bundleCode: bundle.code,
        fromKey: edge.fromKey,
        toKey: edge.toKey,
        relation: edge.relation,
        weight: edge.weight,
        label: edge.label,
        labelAr: edge.labelAr,
      })),
    }),
  ])
}

/** Read a persisted bundle back, for a decision that has already been made. */
export async function loadEvidenceBundle(code: string): Promise<EvidenceBundle | null> {
  const [nodes, edges] = await Promise.all([
    prisma.evidenceNode.findMany({ where: { bundleCode: code }, orderBy: { orderIndex: 'asc' } }),
    prisma.evidenceEdge.findMany({ where: { bundleCode: code } }),
  ])
  if (!nodes.length) return null

  const mapped = nodes.map((node) => ({
    key: node.nodeKey,
    stage: node.stage as EvidenceBundle['nodes'][number]['stage'],
    title: node.title,
    titleAr: node.titleAr,
    detail: node.detail,
    detailAr: node.detailAr,
    sourceType: node.sourceType as DataSource,
    sourceId: node.sourceId,
    value: node.value,
    unit: node.unit,
    confidence: node.confidence,
    freshnessMin: node.freshnessMin,
    modelVersion: node.modelVersion,
    orderIndex: node.orderIndex,
  }))

  const stages = new Set(mapped.map((n) => n.stage))
  const weakest = mapped.filter((n) => n.stage !== 'input').sort((a, b) => a.confidence - b.confidence)[0]
  const measured = mapped.filter((n) => n.sourceType === 'live').length

  return {
    code,
    assetCode: mapped[0]?.sourceId ?? '',
    nodes: mapped,
    edges: edges.map((edge) => ({
      fromKey: edge.fromKey,
      toKey: edge.toKey,
      relation: edge.relation as EvidenceBundle['edges'][number]['relation'],
      weight: edge.weight,
      label: edge.label,
      labelAr: edge.labelAr,
    })),
    weakestLink: weakest
      ? { key: weakest.key, title: weakest.title, titleAr: weakest.titleAr, confidence: weakest.confidence }
      : null,
    missingStages: (
      ['input', 'observation', 'data_quality', 'dna', 'prediction', 'physics', 'twin', 'cascade', 'simulation', 'recommendation', 'impact'] as const
    ).filter((stage) => !stages.has(stage)),
    measuredSharePct: mapped.length ? round((measured / mapped.length) * 100, 1) : 0,
  }
}
