/**
 * Decision memory and governed learning (§27, §28, §29, §30, §32, §33).
 *
 * The claim this module has to earn is "we have seen this before". That is only worth
 * saying if the similarity is computed over things that actually make two situations
 * alike — and if the answer can be explained, because an operator who is told "91 %
 * similar" and cannot see why will, correctly, ignore it.
 *
 * So similarity here is a weighted comparison over named, human-readable fields, and it
 * returns the per-field contributions alongside the score. No embeddings: an opaque
 * vector would be easier to write and impossible to argue with.
 *
 * The hard rule, from §30: a simulated success and an observed success are never merged.
 * They are stored with different evidence kinds, scored differently, and labelled
 * differently everywhere they surface. A prototype that quietly counts its own
 * simulations as prevented failures is making the exact claim §122 forbids.
 */

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

// ─────────────────────────────────────────────────────────────────────────────
// The situation fingerprint
// ─────────────────────────────────────────────────────────────────────────────

export interface SituationContext {
  assetCode: string
  regionCode: string
  assetTypeKey: string
  loadPct: number
  tempC: number
  ambientC: number
  healthScore: number
  riskBefore: number
  hourOfDay: number
  monthOfYear: number
  dnaPatternCode: string
  dnaSimilarity: number
  rootCauseKey: string
  cascadeDepth: number
}

export interface MemoryRecord extends SituationContext {
  code: string
  title: string
  titleAr: string
  strategyKey: string
  strategyLabel: string
  strategyLabelAr: string
  riskAfter: number
  /** simulated | observed — never averaged together (§30). */
  evidenceKind: 'simulated' | 'observed'
  outcomeStatus: 'pending' | 'prevented' | 'occurred' | 'partial' | 'not_observed'
  createdAt: number
}

export interface SimilarityContribution {
  key: string
  label: string
  labelAr: string
  weight: number
  /** 0–1 agreement on this field. */
  agreement: number
  detail: string
  detailAr: string
}

export interface SimilarDecision {
  record: MemoryRecord
  similarity: number
  contributions: SimilarityContribution[]
  /** The single strongest reason these two situations look alike. */
  headline: string
  headlineAr: string
}

// ─────────────────────────────────────────────────────────────────────────────
// Weights
// ─────────────────────────────────────────────────────────────────────────────

const WEIGHTS = {
  assetType: 0.1,
  region: 0.06,
  sameAsset: 0.08,
  loading: 0.16,
  thermal: 0.13,
  ambient: 0.07,
  health: 0.07,
  risk: 0.08,
  timeOfDay: 0.05,
  season: 0.04,
  dna: 0.1,
  rootCause: 0.06,
} as const

const LABELS: Record<keyof typeof WEIGHTS, [string, string]> = {
  assetType: ['Asset type', 'نوع الأصل'],
  region: ['Region', 'المنطقة'],
  sameAsset: ['Same asset', 'الأصل نفسه'],
  loading: ['Loading', 'التحميل'],
  thermal: ['Temperature', 'الحرارة'],
  ambient: ['Ambient conditions', 'الظروف المحيطة'],
  health: ['Asset health', 'صحة الأصل'],
  risk: ['Risk level', 'مستوى الخطر'],
  timeOfDay: ['Time of day', 'وقت اليوم'],
  season: ['Season', 'الموسم'],
  dna: ['Failure DNA', 'بصمة العطل'],
  rootCause: ['Root cause', 'السبب الجذري'],
}

/** Agreement on a continuous field: 1 at identical, falling to 0 at `tolerance` apart. */
const near = (a: number, b: number, tolerance: number): number =>
  clamp(1 - Math.abs(a - b) / tolerance, 0, 1)

/** Hours are circular — 23:00 and 01:00 are two hours apart, not twenty-two. */
const nearHour = (a: number, b: number): number => {
  const diff = Math.min(Math.abs(a - b), 24 - Math.abs(a - b))
  return clamp(1 - diff / 6, 0, 1)
}

/** Months are circular too, and what matters is the season rather than the name. */
const nearMonth = (a: number, b: number): number => {
  const diff = Math.min(Math.abs(a - b), 12 - Math.abs(a - b))
  return clamp(1 - diff / 4, 0, 1)
}

/**
 * How alike are two situations?
 *
 * Note what is *not* weighted heavily: being the same asset contributes eight points out
 * of a hundred. A transformer's history is informative, but the reason to surface a past
 * decision is that the *situation* rhymes — same shape of loading, same heat, same
 * pattern — and restricting the search to one asset would throw away the most useful
 * matches in a fleet of 252.
 */
export function similarity(now: SituationContext, past: MemoryRecord): SimilarDecision {
  const contributions: SimilarityContribution[] = []

  const push = (
    key: keyof typeof WEIGHTS,
    agreement: number,
    detail: string,
    detailAr: string,
  ) => {
    contributions.push({
      key,
      label: LABELS[key][0],
      labelAr: LABELS[key][1],
      weight: WEIGHTS[key],
      agreement: round(clamp(agreement, 0, 1), 3),
      detail,
      detailAr,
    })
  }

  push(
    'assetType',
    now.assetTypeKey === past.assetTypeKey ? 1 : 0,
    now.assetTypeKey === past.assetTypeKey ? `Both ${now.assetTypeKey}` : 'Different asset types',
    now.assetTypeKey === past.assetTypeKey ? 'النوع نفسه' : 'نوعان مختلفان',
  )
  push(
    'region',
    now.regionCode === past.regionCode ? 1 : 0,
    now.regionCode === past.regionCode ? `Both in ${now.regionCode}` : 'Different regions',
    now.regionCode === past.regionCode ? 'المنطقة نفسها' : 'منطقتان مختلفتان',
  )
  push(
    'sameAsset',
    now.assetCode === past.assetCode ? 1 : 0,
    now.assetCode === past.assetCode ? `${now.assetCode} itself` : 'A different unit',
    now.assetCode === past.assetCode ? `${now.assetCode} نفسه` : 'وحدة مختلفة',
  )
  push(
    'loading',
    near(now.loadPct, past.loadPct, 22),
    `${round(now.loadPct, 0)} % now against ${round(past.loadPct, 0)} % then`,
    `${round(now.loadPct, 0)}٪ الآن مقابل ${round(past.loadPct, 0)}٪ حينها`,
  )
  push(
    'thermal',
    near(now.tempC, past.tempC, 18),
    `${round(now.tempC, 0)} °C against ${round(past.tempC, 0)} °C`,
    `${round(now.tempC, 0)}° مقابل ${round(past.tempC, 0)}°`,
  )
  push(
    'ambient',
    near(now.ambientC, past.ambientC, 12),
    `${round(now.ambientC, 0)} °C ambient against ${round(past.ambientC, 0)} °C`,
    `${round(now.ambientC, 0)}° محيطة مقابل ${round(past.ambientC, 0)}°`,
  )
  push(
    'health',
    near(now.healthScore, past.healthScore, 25),
    `Health ${round(now.healthScore, 0)} against ${round(past.healthScore, 0)}`,
    `الصحة ${round(now.healthScore, 0)} مقابل ${round(past.healthScore, 0)}`,
  )
  push(
    'risk',
    near(now.riskBefore, past.riskBefore, 30),
    `Risk ${round(now.riskBefore, 0)} % against ${round(past.riskBefore, 0)} %`,
    `الخطر ${round(now.riskBefore, 0)}٪ مقابل ${round(past.riskBefore, 0)}٪`,
  )
  push(
    'timeOfDay',
    nearHour(now.hourOfDay, past.hourOfDay),
    `${now.hourOfDay}:00 against ${past.hourOfDay}:00`,
    `${now.hourOfDay}:٠٠ مقابل ${past.hourOfDay}:٠٠`,
  )
  push(
    'season',
    nearMonth(now.monthOfYear, past.monthOfYear),
    `Month ${now.monthOfYear} against ${past.monthOfYear}`,
    `الشهر ${now.monthOfYear} مقابل ${past.monthOfYear}`,
  )

  const sameDna = Boolean(now.dnaPatternCode) && now.dnaPatternCode === past.dnaPatternCode
  push(
    'dna',
    sameDna ? clamp(Math.min(now.dnaSimilarity, past.dnaSimilarity) / 100, 0, 1) : 0,
    sameDna ? `Both matched ${now.dnaPatternCode}` : 'Different or absent DNA match',
    sameDna ? `كلاهما طابق ${now.dnaPatternCode}` : 'تطابق مختلف أو غائب',
  )
  push(
    'rootCause',
    Boolean(now.rootCauseKey) && now.rootCauseKey === past.rootCauseKey ? 1 : 0,
    now.rootCauseKey === past.rootCauseKey ? `Both ${now.rootCauseKey}` : 'Different root cause',
    now.rootCauseKey === past.rootCauseKey ? 'السبب الجذري نفسه' : 'سبب جذري مختلف',
  )

  const score = round(
    clamp(
      contributions.reduce((sum, c) => sum + c.agreement * c.weight, 0) * 100,
      0,
      100,
    ),
    1,
  )

  // The headline is the field that contributed the most *actual* agreement, not the one
  // with the biggest weight — so a match driven by an unusual DNA hit says so.
  const top = [...contributions].sort((a, b) => b.agreement * b.weight - a.agreement * a.weight)[0]

  return {
    record: past,
    similarity: score,
    contributions,
    headline: top ? `${top.label}: ${top.detail}` : '',
    headlineAr: top ? `${top.labelAr}: ${top.detailAr}` : '',
  }
}

/**
 * The `n` most similar past decisions.
 *
 * Simulated and observed records are ranked together but never blended: the caller gets
 * the evidence kind on every row and the UI is required to show it.
 */
export function findSimilar(
  now: SituationContext,
  history: MemoryRecord[],
  options: { limit?: number; minSimilarity?: number } = {},
): SimilarDecision[] {
  const limit = options.limit ?? 3
  const min = options.minSimilarity ?? 45

  return history
    .map((record) => similarity(now, record))
    .filter((match) => match.similarity >= min)
    .sort((a, b) => b.similarity - a.similarity)
    .slice(0, limit)
}

// ─────────────────────────────────────────────────────────────────────────────
// Learning candidates (§32, §33)
// ─────────────────────────────────────────────────────────────────────────────

export const LEARNING_KINDS = [
  'pattern',
  'threshold',
  'strategy_preference',
  'dna_refinement',
  'envelope_adjustment',
] as const
export type LearningKind = (typeof LEARNING_KINDS)[number]

export interface LearningCandidate {
  kind: LearningKind
  title: string
  titleAr: string
  reason: string
  reasonAr: string
  evidence: string
  evidenceAr: string
  eventCount: number
  confidence: number
  /** Share of the supporting events that agree, 0–100. */
  supportPct: number
  proposedChange: string
  proposedChangeAr: string
}

/** The minimum number of agreeing events before anything is proposed as a lesson. */
export const MIN_SUPPORT = 3

/**
 * Look for things the system appears to have learned.
 *
 * Nothing here is applied. Every candidate goes to a person, because a model that
 * retrains itself on its own simulated successes is a model that will confidently drift
 * away from reality — and §32 is explicit that production models are not updated without
 * governance.
 *
 * The rules are deliberately conservative: three agreeing events minimum, and simulated
 * evidence is capped well below observed evidence in the confidence it can produce.
 */
export function proposeLearning(history: MemoryRecord[]): LearningCandidate[] {
  const out: LearningCandidate[] = []
  const resolved = history.filter((r) => r.outcomeStatus !== 'pending')
  if (resolved.length < MIN_SUPPORT) return out

  // ── 1. A strategy that keeps winning for one root cause ────────────────────
  const byCause = new Map<string, MemoryRecord[]>()
  for (const record of resolved) {
    if (!record.rootCauseKey) continue
    const list = byCause.get(record.rootCauseKey) ?? []
    list.push(record)
    byCause.set(record.rootCauseKey, list)
  }

  for (const [cause, records] of byCause) {
    if (records.length < MIN_SUPPORT) continue
    const byStrategy = new Map<string, MemoryRecord[]>()
    for (const record of records) {
      const list = byStrategy.get(record.strategyKey) ?? []
      list.push(record)
      byStrategy.set(record.strategyKey, list)
    }
    const [topKey, topRecords] = [...byStrategy.entries()].sort((a, b) => b[1].length - a[1].length)[0]
    if (!topKey || topRecords.length < MIN_SUPPORT) continue

    const supportPct = round((topRecords.length / records.length) * 100, 1)
    if (supportPct < 60) continue

    const observed = topRecords.filter((r) => r.evidenceKind === 'observed').length
    const meanDrop = round(
      topRecords.reduce((sum, r) => sum + (r.riskBefore - r.riskAfter), 0) / topRecords.length,
      1,
    )

    out.push({
      kind: 'strategy_preference',
      title: `Prefer "${topRecords[0].strategyLabel}" for ${cause}`,
      titleAr: `تفضيل "${topRecords[0].strategyLabelAr}" لحالة ${cause}`,
      reason: `${topRecords.length} of ${records.length} decisions with this root cause used it, averaging a ${meanDrop}-point risk reduction.`,
      reasonAr: `${topRecords.length} من ${records.length} قرارًا بهذا السبب الجذري استخدمته، بمتوسط خفض ${meanDrop} نقطة.`,
      evidence: `${observed} observed, ${topRecords.length - observed} simulated`,
      evidenceAr: `${observed} ملاحظ، ${topRecords.length - observed} محاكى`,
      eventCount: topRecords.length,
      confidence: confidenceFor(topRecords, supportPct),
      supportPct,
      proposedChange: `Raise the ranking weight of ${topKey} when the root cause is ${cause}.`,
      proposedChangeAr: `رفع وزن ترتيب ${topKey} عندما يكون السبب الجذري ${cause}.`,
    })
  }

  // ── 2. A pattern whose predictions keep landing early or late ──────────────
  const byPattern = new Map<string, MemoryRecord[]>()
  for (const record of resolved) {
    if (!record.dnaPatternCode) continue
    const list = byPattern.get(record.dnaPatternCode) ?? []
    list.push(record)
    byPattern.set(record.dnaPatternCode, list)
  }

  for (const [pattern, records] of byPattern) {
    if (records.length < MIN_SUPPORT) continue
    const occurred = records.filter((r) => r.outcomeStatus === 'occurred').length
    const prevented = records.filter((r) => r.outcomeStatus === 'prevented').length
    if (occurred === 0 && prevented === 0) continue

    const supportPct = round((Math.max(occurred, prevented) / records.length) * 100, 1)
    if (supportPct < 70) continue

    const leaning = occurred > prevented
    out.push({
      kind: 'dna_refinement',
      title: `${pattern} ${leaning ? 'is under-treated' : 'responds reliably to intervention'}`,
      titleAr: `${pattern} ${leaning ? 'لا يُعالج بما يكفي' : 'يستجيب للتدخل بثبات'}`,
      reason: leaning
        ? `${occurred} of ${records.length} matches went on to occur despite a recommendation being issued.`
        : `${prevented} of ${records.length} matches were prevented by the recommended intervention.`,
      reasonAr: leaning
        ? `${occurred} من ${records.length} تطابقًا وقعت رغم إصدار توصية.`
        : `${prevented} من ${records.length} تطابقًا مُنعت بالتدخل الموصى به.`,
      evidence: `${records.filter((r) => r.evidenceKind === 'observed').length} observed of ${records.length}`,
      evidenceAr: `${records.filter((r) => r.evidenceKind === 'observed').length} ملاحظًا من ${records.length}`,
      eventCount: records.length,
      confidence: confidenceFor(records, supportPct),
      supportPct,
      proposedChange: leaning
        ? `Review the intervention sizing recommended for ${pattern}.`
        : `Increase the confidence attached to ${pattern} matches.`,
      proposedChangeAr: leaning
        ? `مراجعة حجم التدخل الموصى به لـ${pattern}.`
        : `رفع الثقة المرتبطة بتطابقات ${pattern}.`,
    })
  }

  return out.sort((a, b) => b.confidence - a.confidence)
}

/**
 * Confidence in a proposed lesson.
 *
 * Capped hard when the supporting evidence is entirely simulated. A pattern seen twenty
 * times in simulation has been seen zero times in the world, and a governance queue that
 * cannot tell those apart is not governance.
 */
function confidenceFor(records: MemoryRecord[], supportPct: number): number {
  const observed = records.filter((r) => r.evidenceKind === 'observed').length
  const observedShare = observed / records.length
  const volume = clamp(records.length / 12, 0, 1)
  const raw = supportPct * 0.5 + volume * 30 + observedShare * 20
  const ceiling = observed === 0 ? 62 : 95
  return round(clamp(Math.min(raw, ceiling), 0, 100), 1)
}
