/**
 * Decision replay.
 *
 * The timeline of an incident, assembled from the events recorded when the decision was
 * made rather than re-derived on demand. That distinction is the whole design: a replay
 * that recomputed each frame would be a *fresh* analysis wearing a timeline, and could
 * quietly disagree with the decision it claims to be replaying. Reading stored events
 * means the replay is a record, not a re-enactment.
 *
 * The prevention window is the figure the replay exists to show — the gap between the
 * first moment the platform had something to say and the moment the limit would have been
 * crossed. It is computed from the events themselves, so it cannot be asserted.
 *
 * Pure functions. Playback state is derived from a position, so a client can scrub
 * anywhere without the engine holding a cursor.
 */

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

export const REPLAY_STAGES = [
  'observation',
  'weak_signal',
  'dna',
  'prediction',
  'uncertainty',
  'physics',
  'cascade',
  'strategies',
  'simulation',
  'recommendation',
  'decision',
  'outcome',
] as const
export type ReplayStage = (typeof REPLAY_STAGES)[number]

export interface ReplayFrame {
  seq: number
  ts: number
  stage: ReplayStage
  title: string
  titleAr: string
  detail: string
  detailAr: string
  riskScore: number
  loadPct: number
  tempC: number
  state: string
  sourceType: string
  confidence: number
}

export interface ReplayTimeline {
  frames: ReplayFrame[]
  startTs: number
  endTs: number
  durationMin: number
  /** Where the platform first had something to say, and what it was. */
  firstSignal: ReplayFrame | null
  /** The moment a limit would have been crossed without intervention. */
  predictedEvent: ReplayFrame | null
  decision: ReplayFrame | null
  outcome: ReplayFrame | null
  /**
   * Minutes between the first signal and the predicted event — the window an operator
   * actually had. Null when the timeline does not contain both ends, which is the honest
   * answer rather than zero.
   */
  preventionWindowMin: number | null
  /** True while every outcome on the timeline is simulated (§TRUTH RULES). */
  simulatedOnly: boolean
}

/**
 * Build the timeline.
 *
 * Sorted by sequence rather than timestamp: two events can share an instant — a
 * prediction and the uncertainty attached to it are computed together — and the recorded
 * order is the one that tells the story correctly.
 */
export function buildTimeline(frames: ReplayFrame[]): ReplayTimeline {
  const ordered = [...frames].sort((a, b) => a.seq - b.seq)

  if (!ordered.length) {
    return {
      frames: [],
      startTs: 0,
      endTs: 0,
      durationMin: 0,
      firstSignal: null,
      predictedEvent: null,
      decision: null,
      outcome: null,
      preventionWindowMin: null,
      simulatedOnly: true,
    }
  }

  const startTs = Math.min(...ordered.map((frame) => frame.ts))
  const endTs = Math.max(...ordered.map((frame) => frame.ts))

  // The first thing the platform noticed. A weak signal if there was one, because that is
  // the point at which action first became possible; the prediction otherwise.
  const firstSignal =
    ordered.find((frame) => frame.stage === 'weak_signal') ??
    ordered.find((frame) => frame.stage === 'dna') ??
    ordered.find((frame) => frame.stage === 'prediction') ??
    null

  const predictedEvent = ordered.find((frame) => frame.stage === 'prediction') ?? null
  const decision = ordered.find((frame) => frame.stage === 'decision') ?? null
  const outcome = ordered.find((frame) => frame.stage === 'outcome') ?? null

  return {
    frames: ordered,
    startTs,
    endTs,
    durationMin: round((endTs - startTs) / 60_000, 1),
    firstSignal,
    predictedEvent,
    decision,
    outcome,
    preventionWindowMin: null,
    simulatedOnly: ordered.every((frame) => frame.sourceType !== 'live'),
  }
}

/**
 * The window between the first signal and the modelled event.
 *
 * Kept separate from `buildTimeline` because it needs the predicted crossing time, which
 * lives on the prediction rather than on the timeline: the event is a point in the
 * *future* of the last frame, not one of the frames.
 */
export function preventionWindow(
  timeline: ReplayTimeline,
  predictedEventTs: number | null,
): number | null {
  if (!timeline.firstSignal || predictedEventTs === null) return null
  const minutes = (predictedEventTs - timeline.firstSignal.ts) / 60_000
  return minutes > 0 ? round(minutes, 0) : null
}

// ─────────────────────────────────────────────────────────────────────────────
// Playback
// ─────────────────────────────────────────────────────────────────────────────

export const PLAYBACK_SPEEDS = [0.5, 1, 2, 4] as const
export type PlaybackSpeed = (typeof PLAYBACK_SPEEDS)[number]

export interface PlaybackState {
  /** The frame at or before the current position. */
  current: ReplayFrame | null
  /** The next frame, for a "coming up" hint. */
  next: ReplayFrame | null
  index: number
  /** Progress through the timeline, 0–1. */
  progress: number
  atStart: boolean
  atEnd: boolean
}

/**
 * Resolve a position to a frame.
 *
 * Takes a timestamp rather than an index so the twin, the chart and the narration can all
 * be driven from one clock. Scrubbing to a moment between two events shows the earlier
 * one, because that is the state the grid was actually in.
 */
export function frameAt(timeline: ReplayTimeline, ts: number): PlaybackState {
  if (!timeline.frames.length) {
    return { current: null, next: null, index: -1, progress: 0, atStart: true, atEnd: true }
  }

  let index = 0
  for (let i = 0; i < timeline.frames.length; i += 1) {
    if (timeline.frames[i].ts <= ts) index = i
    else break
  }

  const span = timeline.endTs - timeline.startTs
  return {
    current: timeline.frames[index],
    next: timeline.frames[index + 1] ?? null,
    index,
    progress: span > 0 ? clamp((ts - timeline.startTs) / span, 0, 1) : 1,
    atStart: index === 0 && ts <= timeline.startTs,
    atEnd: index === timeline.frames.length - 1,
  }
}

/** Step to the next or previous frame, clamped at both ends. */
export function step(timeline: ReplayTimeline, index: number, delta: number): number {
  return clamp(index + delta, 0, Math.max(0, timeline.frames.length - 1))
}

// ─────────────────────────────────────────────────────────────────────────────
// Before / after / observed (§BEFORE / AFTER / ACTUAL)
// ─────────────────────────────────────────────────────────────────────────────

export type OutcomeKind = 'predicted' | 'simulated' | 'observed' | 'synthetic'

export interface FutureComparison {
  key: 'without' | 'with' | 'observed'
  label: string
  labelAr: string
  riskAt: number
  loadPctAt: number
  tempCAt: number
  state: string
  kind: OutcomeKind
  available: boolean
}

/**
 * The three futures at the end of a replay.
 *
 * The third is deliberately awkward. The brief asks for "ACTUAL" where real data exists —
 * and in this build it never does, so the column is labelled `synthetic` and the word
 * *actual* is not used. Presenting a generated outcome under that heading would be the
 * single most misleading thing this screen could do.
 */
export function compareFutures(input: {
  withoutIntervention: { risk: number; loadPct: number; tempC: number; state: string }
  withStrategy: { risk: number; loadPct: number; tempC: number; state: string }
  observed: { risk: number; loadPct: number; tempC: number; state: string } | null
  observedIsReal: boolean
}): FutureComparison[] {
  const out: FutureComparison[] = [
    {
      key: 'without',
      label: 'Without intervention',
      labelAr: 'دون تدخل',
      riskAt: round(input.withoutIntervention.risk, 1),
      loadPctAt: round(input.withoutIntervention.loadPct, 1),
      tempCAt: round(input.withoutIntervention.tempC, 1),
      state: input.withoutIntervention.state,
      kind: 'predicted',
      available: true,
    },
    {
      key: 'with',
      label: 'With the NABDH strategy',
      labelAr: 'مع استراتيجية نَبْض',
      riskAt: round(input.withStrategy.risk, 1),
      loadPctAt: round(input.withStrategy.loadPct, 1),
      tempCAt: round(input.withStrategy.tempC, 1),
      state: input.withStrategy.state,
      kind: 'simulated',
      available: true,
    },
  ]

  if (input.observed) {
    out.push({
      key: 'observed',
      label: input.observedIsReal ? 'Observed outcome' : 'Synthetic outcome',
      labelAr: input.observedIsReal ? 'النتيجة الملاحظة' : 'نتيجة اصطناعية',
      riskAt: round(input.observed.risk, 1),
      loadPctAt: round(input.observed.loadPct, 1),
      tempCAt: round(input.observed.tempC, 1),
      state: input.observed.state,
      kind: input.observedIsReal ? 'observed' : 'synthetic',
      available: true,
    })
  }

  return out
}

// ─────────────────────────────────────────────────────────────────────────────
// Decision comparison (§Compare Decisions)
// ─────────────────────────────────────────────────────────────────────────────

export interface DecisionOption {
  key: string
  label: string
  labelAr: string
  riskAfter: number
  costSar: number
  executionMin: number
  customerImpact: number
  assetStress: number
  batteryPct: number
  cascadeAvoided: boolean
  confidence: number
}

export interface DecisionComparison {
  options: Array<DecisionOption & { rank: number; isChosen: boolean; deltaRisk: number }>
  chosen: DecisionOption | null
  /** The option that would have done best on risk alone, whether or not it was chosen. */
  lowestRisk: DecisionOption | null
}

/**
 * Rank the alternatives against the one taken.
 *
 * `lowestRisk` is reported separately from the chosen option on purpose: if the operator
 * did not pick the lowest-risk path, a replay that only showed their choice would hide
 * the most interesting question the review could ask.
 */
export function compareDecisions(
  options: DecisionOption[],
  chosenKey: string,
): DecisionComparison {
  if (!options.length) return { options: [], chosen: null, lowestRisk: null }

  const chosen = options.find((option) => option.key === chosenKey) ?? null
  const lowestRisk = [...options].sort((a, b) => a.riskAfter - b.riskAfter)[0] ?? null

  const ranked = [...options]
    .sort((a, b) => a.riskAfter - b.riskAfter || a.costSar - b.costSar)
    .map((option, index) => ({
      ...option,
      rank: index + 1,
      isChosen: option.key === chosenKey,
      deltaRisk: chosen ? round(option.riskAfter - chosen.riskAfter, 1) : 0,
    }))

  return { options: ranked, chosen, lowestRisk }
}
