/**
 * Data quality (§25) and sensor trust (§26).
 *
 * A prediction is only as good as what it was computed from, and the honest thing to do
 * when the inputs are thin is to say so rather than to round the confidence up. These
 * scores are computed from the instrumentation's own record — which sensors reported,
 * how recently, how consistently — and are carried alongside every prediction so a
 * number on screen can always be read together with how much of it is measurement and
 * how much is inference.
 */

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

export type TrustBand = 'trusted' | 'usable' | 'suspect' | 'untrusted'
export type QualityBand = 'excellent' | 'good' | 'limited' | 'low'

/** What the instrumentation record says about one sensor. */
export interface SensorObservation {
  code: string
  kind: string
  /** Declared measurement accuracy, in per cent of reading. */
  accuracy: number
  status: string
  lastSeenAt: number | null
  /** Readings actually stored in the assessment window. */
  readingCount: number
  /** Readings the sampling interval says there should have been. */
  expectedCount: number
  /** How often this instrument is polled, in minutes. */
  samplingIntervalMin?: number
  /** Readings outside the physically plausible range for the channel. */
  outlierCount: number
  installedAt: number
  lastCalibratedAt: number | null
}

export interface SensorTrust {
  code: string
  kind: string
  score: number
  missingRate: number
  noiseLevel: number
  driftPct: number
  outlierRate: number
  band: TrustBand
  /** Human-facing reason the score is not 100, or null when nothing is wrong. */
  reason: string | null
  reasonAr: string | null
}

export interface DataQuality {
  score: number
  band: QualityBand
  sensorsExpected: number
  sensorsReporting: number
  staleSensors: number
  outlierCount: number
  coverageHours: number
  /** Sensors whose trust is low enough that the prediction should be read with care. */
  untrusted: SensorTrust[]
  warning: string | null
  warningAr: string | null
}

const STALE_AFTER_MS = 15 * 60_000
const YEAR_MS = 365.25 * 24 * 3_600_000
/** Calibration interval beyond which drift is assumed to have accumulated. */
const CALIBRATION_INTERVAL_MS = 2 * YEAR_MS

export function trustBand(score: number): TrustBand {
  if (score >= 88) return 'trusted'
  if (score >= 72) return 'usable'
  if (score >= 50) return 'suspect'
  return 'untrusted'
}

export function qualityBand(score: number): QualityBand {
  if (score >= 92) return 'excellent'
  if (score >= 78) return 'good'
  if (score >= 60) return 'limited'
  return 'low'
}

/**
 * Trust in a single sensor.
 *
 * Four independent ways an instrument lets you down — it stops reporting, it reports
 * noise, it drifts out of calibration, it reports impossible values — each penalised on
 * its own so the reason for a low score is legible, not just the score.
 */
export function sensorTrust(observation: SensorObservation, now: number): SensorTrust {
  const expected = Math.max(1, observation.expectedCount)
  const missingRate = clamp(1 - observation.readingCount / expected, 0, 1)
  const outlierRate = clamp(observation.outlierCount / Math.max(1, observation.readingCount), 0, 1)

  // Declared accuracy is a per-cent-of-reading figure; treat it directly as noise.
  const noiseLevel = clamp(observation.accuracy / 5, 0, 1)

  const sinceCalibration = observation.lastCalibratedAt
    ? now - observation.lastCalibratedAt
    : now - observation.installedAt
  const driftPct = round(clamp((sinceCalibration / CALIBRATION_INTERVAL_MS) * 3.2, 0, 12), 2)

  const isStale = observation.lastSeenAt === null || now - observation.lastSeenAt > STALE_AFTER_MS
  const statusPenalty = observation.status === 'healthy' ? 0 : observation.status === 'degraded' ? 18 : 40

  const score = round(
    clamp(
      100 -
        missingRate * 42 -
        outlierRate * 26 -
        noiseLevel * 12 -
        driftPct * 1.6 -
        statusPenalty -
        (isStale ? 14 : 0),
      0,
      100,
    ),
    1,
  )

  let reason: string | null = null
  let reasonAr: string | null = null
  if (statusPenalty > 0) {
    reason = `Sensor reported as ${observation.status}`
    reasonAr = 'الحساس مُبلَّغ عنه بحالة غير سليمة'
  } else if (isStale) {
    reason = 'No reading within the last 15 minutes'
    reasonAr = 'لا توجد قراءة خلال آخر ١٥ دقيقة'
  } else if (missingRate > 0.15) {
    reason = `${Math.round(missingRate * 100)} % of expected readings missing`
    reasonAr = `${Math.round(missingRate * 100)}٪ من القراءات المتوقعة مفقودة`
  } else if (outlierRate > 0.05) {
    reason = `${Math.round(outlierRate * 100)} % of readings outside the plausible range`
    reasonAr = `${Math.round(outlierRate * 100)}٪ من القراءات خارج النطاق المعقول`
  } else if (driftPct > 4) {
    reason = `Calibration drift estimated at ${driftPct} %`
    reasonAr = `انحراف المعايرة المقدّر ${driftPct}٪`
  }

  return {
    code: observation.code,
    kind: observation.kind,
    score,
    missingRate: round(missingRate, 4),
    noiseLevel: round(noiseLevel, 4),
    driftPct,
    outlierRate: round(outlierRate, 4),
    band: trustBand(score),
    reason,
    reasonAr,
  }
}

/**
 * Aggregate data confidence for one asset.
 *
 * Weighted towards the worst instrument rather than the average: a prediction that leans
 * on a channel nobody trusts is not rescued by four healthy channels beside it.
 */
export function assessDataQuality(
  observations: SensorObservation[],
  now: number,
  options: { expectedSensors?: number; coverageHours?: number } = {},
): DataQuality {
  const expectedSensors = options.expectedSensors ?? observations.length
  const coverageHours = options.coverageHours ?? 0
  const trusts = observations.map((observation) => sensorTrust(observation, now))

  const reporting = observations.filter(
    (o) => o.lastSeenAt !== null && now - o.lastSeenAt <= STALE_AFTER_MS,
  ).length
  const stale = observations.length - reporting
  const outlierCount = observations.reduce((sum, o) => sum + o.outlierCount, 0)

  const mean = trusts.length > 0 ? trusts.reduce((sum, t) => sum + t.score, 0) / trusts.length : 0
  const worst = trusts.length > 0 ? Math.min(...trusts.map((t) => t.score)) : 0
  const completeness = expectedSensors > 0 ? clamp(observations.length / expectedSensors, 0, 1) : 0
  // 24 hours of history is treated as full coverage for a short-horizon prediction.
  const coverage = clamp(coverageHours / 24, 0, 1)

  // Weighted towards the mean but pulled down by the worst instrument, because a fleet
  // is only as reliable as the channel a prediction happens to depend on. The two
  // weights sum to one: they were 0.55 and 0.25, which capped a flawless fleet at 80 and
  // meant "excellent" was unreachable by construction.
  const trustScore = mean * 0.7 + worst * 0.3

  const score =
    trusts.length === 0
      ? 0
      : round(clamp(trustScore * completeness * (0.75 + 0.25 * coverage), 0, 100), 1)

  const untrusted = trusts
    .filter((t) => t.band === 'suspect' || t.band === 'untrusted')
    .sort((a, b) => a.score - b.score)

  const band = qualityBand(score)
  let warning: string | null = null
  let warningAr: string | null = null
  if (band === 'low') {
    warning = 'Low data confidence — treat this prediction as indicative only.'
    warningAr = 'ثقة منخفضة في البيانات — تعامل مع هذا التنبؤ كمؤشر فقط.'
  } else if (band === 'limited') {
    warning = 'Limited data confidence — some inputs are stale or unverified.'
    warningAr = 'ثقة محدودة في البيانات — بعض المدخلات قديمة أو غير مُتحقق منها.'
  } else if (untrusted.length > 0) {
    warning = `${untrusted.length} sensor(s) below the trust threshold.`
    warningAr = `${untrusted.length} حساس دون عتبة الثقة.`
  }

  return {
    score,
    band,
    sensorsExpected: expectedSensors,
    sensorsReporting: reporting,
    staleSensors: stale,
    outlierCount,
    coverageHours: round(coverageHours, 1),
    untrusted,
    warning,
    warningAr,
  }
}

/**
 * The four confidences a decision carries (§15), and the single number that must not be
 * higher than its weakest input.
 */
export interface DecisionConfidence {
  recommendation: number
  data: number
  prediction: number
  simulation: number
  overall: number
  band: QualityBand
  isLowData: boolean
}

export function decisionConfidence(input: {
  recommendation: number
  data: number
  prediction: number
  simulation: number
}): DecisionConfidence {
  const values = [input.recommendation, input.data, input.prediction, input.simulation].map((v) =>
    clamp(v, 0, 100),
  )
  const mean = values.reduce((sum, v) => sum + v, 0) / values.length
  const weakest = Math.min(...values)
  // Capped by the weakest link: averaging alone would let a confident simulation paper
  // over inputs nobody can vouch for.
  const overall = round(Math.min(mean, weakest + 12), 1)
  return {
    recommendation: round(values[0], 1),
    data: round(values[1], 1),
    prediction: round(values[2], 1),
    simulation: round(values[3], 1),
    overall,
    band: qualityBand(overall),
    isLowData: input.data < 60,
  }
}
