/**
 * Weak signal detection (§33).
 *
 * A threshold alarm asks one question of one channel: is this number past the line yet?
 * By the time the answer is yes, the margin is already gone. The interesting information
 * arrives earlier, spread thin across several channels at once — temperature three per
 * cent above where the twin says it should be, loading four per cent above, cooling a
 * couple of per cent down. None of those trips anything. Together they are a shape.
 *
 * This module measures each channel against the digital twin's expectation for the same
 * asset at the same hour under baseline conditions, and reports both the individual
 * deviations and their combined strength — plus, explicitly, whether a conventional
 * threshold alarm would have fired yet. That last flag is what makes the lead-time claim
 * in §34 checkable rather than rhetorical.
 */

import type { EnvironmentSample } from './environment'
import { clamp, round } from './rng'
import {
  NO_PERTURBATION,
  sampleAsset,
  type AssetProfile,
  type Perturbation,
  type TelemetrySample,
} from './telemetry'

export const WEAK_SIGNAL_KEYS = [
  'load',
  'temperature',
  'voltage',
  'cooling',
  'health',
] as const
export type WeakSignalKey = (typeof WEAK_SIGNAL_KEYS)[number]

export interface WeakSignal {
  key: WeakSignalKey
  /** Deviation from the twin's expectation, in per cent of the expected value. */
  deviationPct: number
  /** Deviation at which this channel alone would be worth an alarm. */
  individualThresholdPct: number
  /** True when this one channel is already past its own threshold. */
  isIndividuallyCritical: boolean
  weight: number
  /** Share of the combined score contributed by this channel, in points. */
  contribution: number
  observed: number
  expected: number
  unit: string
}

export interface WeakSignalResult {
  signals: WeakSignal[]
  /** Combined strength of the pattern, 0–100. */
  combinedScore: number
  /** True once the pattern is strong enough to be worth acting on. */
  isPattern: boolean
  /** True when a conventional threshold alarm would already be sounding. */
  traditionalAlarm: boolean
  traditionalReason: string | null
}

/** Weights reflect how early each channel moves, not how alarming it looks at the end. */
const WEIGHTS: Record<WeakSignalKey, number> = {
  load: 0.26,
  temperature: 0.3,
  voltage: 0.16,
  cooling: 0.18,
  health: 0.1,
}

/** Per-channel thresholds a classic supervisory alarm would use. */
const INDIVIDUAL_THRESHOLD_PCT: Record<WeakSignalKey, number> = {
  load: 18,
  temperature: 15,
  voltage: 40,
  cooling: 12,
  health: 10,
}

/** The combined score above which the pattern is reported as real. */
export const WEAK_SIGNAL_PATTERN_THRESHOLD = 45

/**
 * Cooling effectiveness: how much of the temperature rise above ambient the unit is
 * actually shedding. A unit whose rise grows while its loading does not is losing
 * cooling, which no single channel shows on its own.
 */
function coolingEffectiveness(sample: TelemetrySample): number {
  const rise = Math.max(0.1, sample.tempC - sample.ambientC)
  const loading = Math.max(0.05, Math.abs(sample.loadPct) / 100)
  // Rise per unit of loading; lower is better cooling.
  return rise / loading
}

function relativeDeviation(observed: number, expected: number): number {
  const base = Math.max(Math.abs(expected), 0.5)
  return ((observed - expected) / base) * 100
}

/**
 * Compare an asset's live behaviour with the twin's baseline expectation.
 *
 * `baseline` is the same asset, at the same instant, with the scenario removed — so the
 * deviations isolate what the scenario is doing rather than what the hour of day does.
 */
export function detectWeakSignals(
  profile: AssetProfile,
  sample: TelemetrySample,
  env: EnvironmentSample,
  options: { baseline?: TelemetrySample; perturbation?: Perturbation } = {},
): WeakSignalResult {
  const baseline =
    options.baseline ?? sampleAsset(profile, sample.ts, { ...env, tempC: env.tempC }, NO_PERTURBATION)

  const raw: Array<{ key: WeakSignalKey; observed: number; expected: number; unit: string }> = [
    { key: 'load', observed: Math.abs(sample.loadPct), expected: Math.abs(baseline.loadPct), unit: '%' },
    { key: 'temperature', observed: sample.tempC, expected: baseline.tempC, unit: '°C' },
    {
      key: 'voltage',
      observed: Math.abs(sample.voltageDeviationPct),
      expected: Math.abs(baseline.voltageDeviationPct),
      unit: '%',
    },
    {
      key: 'cooling',
      observed: coolingEffectiveness(sample),
      expected: coolingEffectiveness(baseline),
      unit: 'K/pu',
    },
    { key: 'health', observed: baseline.effectiveHealth, expected: sample.effectiveHealth, unit: '%' },
  ]

  const signals: WeakSignal[] = raw.map((entry) => {
    const deviationPct = round(relativeDeviation(entry.observed, entry.expected), 2)
    const threshold = INDIVIDUAL_THRESHOLD_PCT[entry.key]
    // Only adverse excursions count. Running cooler or lighter than expected is good
    // news, and averaging it in would let one healthy channel hide four sick ones.
    const adverse = Math.max(0, deviationPct)
    return {
      key: entry.key,
      deviationPct,
      individualThresholdPct: threshold,
      isIndividuallyCritical: adverse >= threshold,
      weight: WEIGHTS[entry.key],
      contribution: round(clamp(adverse / threshold, 0, 1.6) * WEIGHTS[entry.key] * 100, 1),
      observed: round(entry.observed, 2),
      expected: round(entry.expected, 2),
      unit: entry.unit,
    }
  })

  const combinedScore = round(
    clamp(
      signals.reduce((sum, signal) => sum + signal.contribution, 0),
      0,
      100,
    ),
    1,
  )

  // What a conventional alarm would see: absolute limits on single channels, with no
  // reference to what this asset was expected to be doing.
  let traditionalReason: string | null = null
  if (Math.abs(sample.loadPct) >= 100) traditionalReason = 'overload'
  else if (sample.tempC >= profile.ratedTempC) traditionalReason = 'temperature'
  else if (Math.abs(sample.voltageDeviationPct) >= 5) traditionalReason = 'voltage'

  return {
    signals: [...signals].sort((a, b) => b.contribution - a.contribution),
    combinedScore,
    isPattern: combinedScore >= WEAK_SIGNAL_PATTERN_THRESHOLD,
    traditionalAlarm: traditionalReason !== null,
    traditionalReason,
  }
}

/**
 * How much earlier the pattern was visible than the threshold alarm.
 *
 * Walks backwards minute by minute and reports the first moment at which the combined
 * pattern was already strong while no single channel had crossed its limit. That
 * difference is the prevention window the platform actually buys (§34).
 */
export function weakSignalLeadMinutes(
  profile: AssetProfile,
  ts: number,
  envAt: (ts: number) => EnvironmentSample,
  perturbation: Perturbation,
  options: { lookBackMin?: number; stepMin?: number } = {},
): { patternAtMin: number | null; alarmAtMin: number | null; leadMinutes: number } {
  const lookBackMin = options.lookBackMin ?? 180
  const stepMin = options.stepMin ?? 1
  let patternAtMin: number | null = null
  let alarmAtMin: number | null = null

  for (let offset = -lookBackMin; offset <= 0; offset += stepMin) {
    const at = ts + offset * 60_000
    const env = envAt(at)
    const sample = sampleAsset(profile, at, env, perturbation)
    const result = detectWeakSignals(profile, sample, env, {
      baseline: sampleAsset(profile, at, env, NO_PERTURBATION),
    })
    if (patternAtMin === null && result.isPattern) patternAtMin = offset
    if (alarmAtMin === null && result.traditionalAlarm) alarmAtMin = offset
    if (patternAtMin !== null && alarmAtMin !== null) break
  }

  const leadMinutes =
    patternAtMin !== null && alarmAtMin !== null ? Math.max(0, alarmAtMin - patternAtMin) : 0

  return { patternAtMin, alarmAtMin, leadMinutes }
}
