/**
 * Cross-sensor validation (§79, §80).
 *
 * Trust based on a sensor's own record — is it reporting, is it noisy, is it overdue for
 * calibration — has a blind spot: an instrument that reports steadily, cleanly and on
 * schedule, and is wrong. Nothing in its own history reveals that. Only comparison does.
 *
 * So this module asks three questions about every reading:
 *
 *   1. Do neighbouring sensors of the same kind agree?
 *   2. Does it agree with its own recent history?
 *   3. Does it agree with what the digital twin expects?
 *
 * Disagreement with one of the three is a hint. Disagreement with all three is a sensor
 * fault. Agreement with the neighbours while disagreeing with the twin is the opposite
 * finding — the instrument is fine and the *model* is wrong — and reporting that the
 * right way round is the whole point of doing this (§8).
 */

import { clamp, round } from './rng'

export const VALIDATION_VERDICTS = [
  'consistent',
  'sensor_suspect',
  'model_suspect',
  'asset_behaviour',
  'insufficient_reference',
] as const
export type ValidationVerdict = (typeof VALIDATION_VERDICTS)[number]

export interface PeerReading {
  code: string
  value: number
  /** Trust in the peer itself, 0–100 — an untrusted peer is a weak witness. */
  trust: number
}

export interface CrossSensorInput {
  code: string
  kind: string
  assetCode: string
  value: number
  unit: string
  /** Same channel, physically adjacent plant. */
  peers: PeerReading[]
  /** This sensor's own recent readings, oldest first. */
  history: number[]
  /** What the twin says this channel should be reading. */
  twinExpected: number | null
  /** Fidelity of the twin producing that expectation, 0–100. */
  twinFidelity: number
  /** Trust from the sensor's own record, 0–100. */
  baseTrust: number
}

export interface CrossSensorResult {
  code: string
  assetCode: string
  verdict: ValidationVerdict
  /** Trust after the comparison. Can move in either direction. */
  adjustedTrust: number
  /** Deviation from the peer median, as a share of the peer spread. */
  peerZScore: number | null
  peerMedian: number | null
  /** Deviation from the sensor's own recent mean, in units. */
  selfDeviation: number | null
  /** Deviation from the twin's expectation, in units. */
  twinDeviation: number | null
  confidence: number
  reason: string
  reasonAr: string
}

function median(values: number[]): number {
  const sorted = [...values].sort((a, b) => a - b)
  const mid = Math.floor(sorted.length / 2)
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
}

/**
 * Median absolute deviation, used instead of the standard deviation on purpose.
 *
 * If two sensors on a bus have both failed the same way, the mean and the standard
 * deviation both move with them and the outlier test stops working. The median and the
 * MAD do not — that is the entire reason robust statistics exist.
 */
function mad(values: number[], centre: number): number {
  if (!values.length) return 0
  return median(values.map((v) => Math.abs(v - centre)))
}

/** Below this many trusted peers, a peer comparison is not evidence. */
const MIN_PEERS = 2
/** Peer deviation beyond this many robust units is an outlier. */
const OUTLIER_Z = 3.5
/** Twin disagreement beyond this share of the expected value is material. */
const TWIN_TOLERANCE = 0.08

/**
 * Validate one reading against everything that can speak to it.
 *
 * The verdict order matters. A reading that disagrees with its neighbours *and* its own
 * history is almost certainly the instrument. A reading that agrees with its neighbours
 * but not the twin is almost certainly the model. Checking the peers first is what keeps
 * those two apart.
 */
export function validateAgainstPeers(input: CrossSensorInput): CrossSensorResult {
  const trustedPeers = input.peers.filter((p) => p.trust >= 55)

  let peerMedian: number | null = null
  let peerZ: number | null = null
  if (trustedPeers.length >= MIN_PEERS) {
    const values = trustedPeers.map((p) => p.value)
    peerMedian = round(median(values), 3)
    const spread = mad(values, peerMedian)
    // A perfectly uniform peer set has zero spread, which would make every deviation
    // infinite. Floor it at something small but real for the channel's scale.
    const scale = Math.max(spread * 1.4826, Math.abs(peerMedian) * 0.02, 0.05)
    peerZ = round((input.value - peerMedian) / scale, 2)
  }

  const selfMean = input.history.length
    ? input.history.reduce((a, b) => a + b, 0) / input.history.length
    : null
  const selfDeviation = selfMean === null ? null : round(input.value - selfMean, 3)
  const selfSpread = selfMean === null ? 0 : mad(input.history, selfMean) * 1.4826
  const selfOutlier =
    selfDeviation !== null && Math.abs(selfDeviation) > Math.max(selfSpread * 3.5, Math.abs(selfMean ?? 1) * 0.12)

  const twinDeviation =
    input.twinExpected === null ? null : round(input.value - input.twinExpected, 3)
  const twinScale = input.twinExpected === null ? 1 : Math.max(Math.abs(input.twinExpected), 1)
  const twinDisagrees =
    twinDeviation !== null &&
    input.twinFidelity >= 55 &&
    Math.abs(twinDeviation) / twinScale > TWIN_TOLERANCE

  const peerDisagrees = peerZ !== null && Math.abs(peerZ) > OUTLIER_Z

  // ── Verdict ───────────────────────────────────────────────────────────────
  let verdict: ValidationVerdict
  let trustDelta: number
  let confidence: number
  let reason: string
  let reasonAr: string

  if (peerZ === null && input.twinExpected === null) {
    verdict = 'insufficient_reference'
    trustDelta = 0
    confidence = 20
    reason = `No trusted peer on the same channel and no twin expectation — this reading cannot be checked against anything`
    reasonAr = 'لا نظير موثوق على القناة نفسها ولا توقّع من التوأم — لا يمكن التحقق من هذه القراءة'
  } else if (peerDisagrees && selfOutlier) {
    verdict = 'sensor_suspect'
    trustDelta = -28
    confidence = 88
    reason = `${round(input.value, 2)} ${input.unit} is ${Math.abs(peerZ!)} robust units from the peer median of ${peerMedian} and a step change against its own history — the instrument, not the plant`
    reasonAr = `${round(input.value, 2)} ${input.unit} تبعد ${Math.abs(peerZ!)} وحدة عن وسيط النظراء ${peerMedian} وتمثل قفزة عن تاريخها — الجهاز لا المعدّة`
  } else if (peerDisagrees && !selfOutlier) {
    // Off from the neighbours but consistent with itself: either a long-standing
    // calibration offset or a genuinely different operating point.
    verdict = 'sensor_suspect'
    trustDelta = -16
    confidence = 64
    reason = `Consistently ${round(input.value - (peerMedian ?? 0), 2)} ${input.unit} away from its peers — reads like a calibration offset rather than an event`
    reasonAr = `تبتعد باستمرار ${round(input.value - (peerMedian ?? 0), 2)} ${input.unit} عن نظرائها — يبدو انحراف معايرة لا حدثًا`
  } else if (twinDisagrees && !peerDisagrees && selfOutlier) {
    // Everything moved together, away from the model. Checked *before* the stale-model
    // case below, because both look like "peers agree, twin does not" and the only thing
    // that separates them is whether the reading moved: a steady offset means the model
    // is behind, a sharp move with the peers following means the plant is doing something
    // the model does not expect. Getting this order wrong reports every real event as a
    // modelling error.
    verdict = 'asset_behaviour'
    trustDelta = 0
    confidence = 70
    reason = `Moved ${round(selfDeviation ?? 0, 2)} ${input.unit} against its own history and away from the twin, with peers following — the asset is behaving differently`
    reasonAr = `تحركت ${round(selfDeviation ?? 0, 2)} ${input.unit} عن تاريخها وبعيدًا عن التوأم مع متابعة النظراء — سلوك الأصل تغيّر`
  } else if (twinDisagrees && !peerDisagrees && peerZ !== null) {
    // The instruments agree with each other and hold steady; the model is the odd one out.
    verdict = 'model_suspect'
    trustDelta = +4
    confidence = 78
    reason = `Agrees with ${trustedPeers.length} peers but sits ${round(twinDeviation ?? 0, 2)} ${input.unit} from the twin's expectation — the model is behind the plant`
    reasonAr = `تتفق مع ${trustedPeers.length} نظراء لكنها تبعد ${round(twinDeviation ?? 0, 2)} ${input.unit} عن توقّع التوأم — النموذج متأخر عن الواقع`
  } else if (twinDisagrees) {
    verdict = 'model_suspect'
    trustDelta = 0
    confidence = 52
    reason = `${round(Math.abs(twinDeviation ?? 0), 2)} ${input.unit} from the twin's expectation, with no peer evidence either way`
    reasonAr = `تبعد ${round(Math.abs(twinDeviation ?? 0), 2)} ${input.unit} عن توقّع التوأم دون دليل من النظراء`
  } else {
    verdict = 'consistent'
    // Corroboration is worth something: a reading confirmed by peers and the model is
    // more trustworthy than one that simply has not been contradicted.
    trustDelta = peerZ !== null && input.twinExpected !== null ? +6 : +2
    confidence = 82
    reason =
      peerZ !== null
        ? `Within ${Math.abs(peerZ)} robust units of ${trustedPeers.length} peers and consistent with the twin`
        : `Consistent with the twin's expectation`
    reasonAr =
      peerZ !== null
        ? `ضمن ${Math.abs(peerZ)} وحدة من ${trustedPeers.length} نظراء ومتسقة مع التوأم`
        : 'متسقة مع توقّع التوأم'
  }

  return {
    code: input.code,
    assetCode: input.assetCode,
    verdict,
    adjustedTrust: round(clamp(input.baseTrust + trustDelta, 0, 100), 1),
    peerZScore: peerZ,
    peerMedian,
    selfDeviation,
    twinDeviation,
    confidence,
    reason,
    reasonAr,
  }
}

/**
 * Roll a set of validations into a health picture for a page or a panel.
 *
 * `unverifiable` is reported separately from `suspect` because they are different
 * problems: one is an instrument that is probably wrong, the other is an instrument
 * nobody can check. A fleet with many of the second has a coverage problem, not a
 * calibration problem.
 */
export interface CrossSensorSummary {
  total: number
  consistent: number
  sensorSuspect: number
  modelSuspect: number
  assetBehaviour: number
  unverifiable: number
  /** Share of readings that were corroborated by something, 0–100. */
  corroboratedPct: number
  headline: string
  headlineAr: string
}

export function summariseCrossSensor(results: CrossSensorResult[]): CrossSensorSummary {
  const count = (v: ValidationVerdict) => results.filter((r) => r.verdict === v).length
  const total = results.length
  const unverifiable = count('insufficient_reference')
  const corroborated = total - unverifiable

  const sensorSuspect = count('sensor_suspect')
  const modelSuspect = count('model_suspect')

  return {
    total,
    consistent: count('consistent'),
    sensorSuspect,
    modelSuspect,
    assetBehaviour: count('asset_behaviour'),
    unverifiable,
    corroboratedPct: total ? round((corroborated / total) * 100, 1) : 0,
    headline:
      sensorSuspect > 0
        ? `${sensorSuspect} reading${sensorSuspect === 1 ? '' : 's'} contradicted by their peers`
        : modelSuspect > 0
          ? `${modelSuspect} reading${modelSuspect === 1 ? '' : 's'} where the instruments agree and the model does not`
          : unverifiable === total && total > 0
            ? 'No reading in this set could be corroborated'
            : 'All corroborated readings are consistent',
    headlineAr:
      sensorSuspect > 0
        ? `${sensorSuspect} قراءة يناقضها نظراؤها`
        : modelSuspect > 0
          ? `${modelSuspect} قراءة تتفق فيها الأجهزة ويختلف النموذج`
          : unverifiable === total && total > 0
            ? 'لا يمكن تعزيز أي قراءة في هذه المجموعة'
            : 'كل القراءات المعزَّزة متسقة',
  }
}
