///|
/// Phase 2.2 (ADR 0006 ยง Context-loss recovery): cross-platform scaffolding
/// for monitoring GPU context-loss signals and deciding when to fall back
/// to `SkiaRasterNative`. Per-platform loss detectors (Metal
/// `MTLDevice deviceLostHandler`, Vulkan `VK_ERROR_DEVICE_LOST`, EGL
/// `EGL_CONTEXT_LOST`, D3D12 `GetDeviceRemovedReason`) feed this module via
/// `report_context_loss`; the renderer thread consults `should_recover` and
/// `should_fallback_to_raster` each frame to drive cache tear-down,
/// context recreation, and the final raster fallback. AppRuntime state is
/// preserved across all transitions: only the renderer-thread-owned GPU
/// resources are recycled.
///
/// The recovery state machine is:
///   Idle -> Lost (context loss reported; 3 VSync deadline starts)
///        -> Recovering (recreation in flight)
///        -> Recovered (recreation succeeded; GPU route resumes)
///        | FallbackToRaster (repeated failure exceeded the budget)
/// The deadline is `3 * vsync_ms` after the loss was reported; on expiry
/// without recovery, the renderer thread falls back to raster.
pub(all) enum RendererRecoveryState {
  /// No context loss has been reported; the GPU route is healthy.
  Idle
  /// A context-loss signal was received. The renderer thread must tear down
  /// GPU caches and recreate the context + surface before
  /// `recovery_deadline_ms`. The payload is the loss reason reported by the
  /// platform detector and the deadline (in milliseconds, host clock).
  Lost(String, Double)
  /// Context + surface recreation is in flight. The renderer thread must
  /// not present GPU frames while in this state; it should drain the
  /// mailbox without beginning new GPU work.
  Recovering
  /// Recreation succeeded; the GPU route is healthy again. The renderer
  /// thread resumes GPU presentation on the next frame.
  Recovered
  /// Repeated recovery failure exceeded the consecutive budget. The
  /// renderer thread must fall back to `SkiaRasterNative` while preserving
  /// `AppRuntime` state. The payload is the final failure reason.
  FallbackToRaster(String)
} derive(Eq, Debug, ToJson)

///|
pub extend RendererRecoveryState with Debug::{to_repr}

///|
pub extend RendererRecoveryState with Eq::{not_equal, equal}

///|
pub extend RendererRecoveryState with ToJson::{to_json}

///|
/// Tunable recovery budget. Per ADR 0006, context-loss recovery must
/// complete within 3 VSyncs. Repeated failure above
/// `max_consecutive_recovery_failures` triggers the raster fallback.
pub(all) struct RendererRecoveryBudget {
  /// VSync interval in milliseconds. Default 16.67 ms (60 Hz).
  vsync_ms : Double
  /// Recovery deadline multiplier. The deadline is
  /// `vsync_ms * recovery_vsync_budget` (default 3).
  recovery_vsync_budget : Int
  /// Maximum consecutive recovery failures before falling back to raster.
  /// Default 2 per ADR 0006 (recovery is retried once; the second failure
  /// falls back).
  max_consecutive_recovery_failures : Int
} derive(Eq, Debug, ToJson)

///|
pub extend RendererRecoveryBudget with Debug::{to_repr}

///|
pub extend RendererRecoveryBudget with Eq::{not_equal, equal}

///|
pub extend RendererRecoveryBudget with ToJson::{to_json}

///|
pub struct RendererRecovery {
  mut state : RendererRecoveryState
  mut loss_count : Int
  mut last_loss_reason : String
  mut last_loss_time_ms : Double
  mut consecutive_failures : Int
  mut recovery_started_for_loss : Bool
  budget : RendererRecoveryBudget
}

///|
pub fn RendererRecoveryBudget::default() -> RendererRecoveryBudget {
  {
    vsync_ms: 16.67,
    recovery_vsync_budget: 3,
    max_consecutive_recovery_failures: 2,
  }
}

///|
/// Builds a custom recovery budget. Useful for testing platform-specific
/// VSync intervals (e.g. 120 Hz) and tighter/looser recovery windows.
pub fn RendererRecoveryBudget::new(
  vsync_ms~ : Double,
  recovery_vsync_budget~ : Int,
  max_consecutive_recovery_failures~ : Int,
) -> RendererRecoveryBudget {
  { vsync_ms, recovery_vsync_budget, max_consecutive_recovery_failures, }
}

///|
pub fn RendererRecoveryBudget::recovery_deadline_ms(
  self : RendererRecoveryBudget,
  loss_time_ms : Double,
) -> Double {
  loss_time_ms + self.vsync_ms * Double::from_int(self.recovery_vsync_budget)
}

///|
pub fn RendererRecovery::new(
  budget? : RendererRecoveryBudget = RendererRecoveryBudget::default(),
) -> RendererRecovery {
  {
    state: Idle,
    loss_count: 0,
    last_loss_reason: "",
    last_loss_time_ms: 0.0,
    consecutive_failures: 0,
    recovery_started_for_loss: false,
    budget,
  }
}

///|
/// Returns the current recovery state. The renderer thread consults this
/// each frame to decide whether to render GPU work, drain only, or fall
/// back to raster.
pub fn RendererRecovery::state(
  self : RendererRecovery,
) -> RendererRecoveryState {
  self.state
}

///|
/// Returns the total number of context-loss events observed since
/// construction. Used by promotion manifests (Phase 2.3) as evidence that
/// recovery has been exercised on matching hardware.
pub fn RendererRecovery::loss_count(self : RendererRecovery) -> Int {
  self.loss_count
}

///|
/// Returns the most recent loss reason reported by a platform detector,
/// or the empty string if no loss has been observed.
pub fn RendererRecovery::last_loss_reason(self : RendererRecovery) -> String {
  self.last_loss_reason
}

///|
/// Returns the number of consecutive recovery failures since the last
/// successful recovery. Resets to 0 on `report_recovery_succeeded`.
pub fn RendererRecovery::consecutive_failures(self : RendererRecovery) -> Int {
  self.consecutive_failures
}

///|
/// Returns true when the renderer thread should fall back to
/// `SkiaRasterNative`. Once true, the state is terminal: the renderer
/// thread must not attempt GPU recreation again without an external reset
/// (e.g. a host-driven surface recreation that re-initializes the
/// renderer thread).
pub fn RendererRecovery::should_fallback_to_raster(
  self : RendererRecovery,
) -> Bool {
  match self.state {
    FallbackToRaster(_) => true
    _ => false
  }
}

///|
/// Returns true when the renderer thread should attempt GPU context +
/// surface recreation this frame. The caller must call
/// `report_recovery_started` before beginning recreation and
/// `report_recovery_succeeded` / `report_recovery_failed` once the attempt
/// completes.
pub fn RendererRecovery::should_recover(
  self : RendererRecovery,
  now_ms~ : Double,
) -> Bool {
  match self.state {
    Lost(_, deadline) => now_ms <= deadline
    _ => false
  }
}

///|
/// Returns true when the recovery deadline has expired without a
/// successful recovery. The renderer thread should call
/// `report_recovery_failed` to advance to the next attempt (or the raster
/// fallback).
pub fn RendererRecovery::recovery_deadline_expired(
  self : RendererRecovery,
  now_ms~ : Double,
) -> Bool {
  match self.state {
    Lost(_, deadline) => now_ms > deadline
    _ => false
  }
}

///|
/// Reports a context-loss signal received from the platform detector.
/// Transitions from `Idle` or `Recovered` to `Lost`, stamping the recovery
/// deadline. Calling from `Lost`/`Recovering` records a new loss (the
/// in-flight recovery is treated as failed). Calling from
/// `FallbackToRaster` is a no-op: the renderer thread is no longer on the
/// GPU route.
pub fn RendererRecovery::report_context_loss(
  self : RendererRecovery,
  reason : String,
  now_ms~ : Double,
) -> Unit {
  match self.state {
    FallbackToRaster(_) => return
    _ => ()
  }
  // If a recovery was in flight, count it as a failure.
  if self.recovery_started_for_loss {
    self.consecutive_failures = self.consecutive_failures + 1
    self.recovery_started_for_loss = false
  }
  self.loss_count = self.loss_count + 1
  self.last_loss_reason = reason
  self.last_loss_time_ms = now_ms
  let deadline = self.budget.recovery_deadline_ms(now_ms)
  self.state = Lost(reason, deadline)
}

///|
/// Marks the start of a GPU context + surface recreation attempt. The
/// renderer thread must call this before recreating the context so that
/// subsequent `report_context_loss` calls (e.g. a loss detected during
/// recreation) count as a recovery failure.
pub fn RendererRecovery::report_recovery_started(
  self : RendererRecovery,
) -> Unit {
  match self.state {
    Lost(_, _) => {
      self.state = Recovering
      self.recovery_started_for_loss = true
    }
    _ => ()
  }
}

///|
/// Reports that GPU context + surface recreation succeeded. Resets the
/// consecutive failure count and transitions to `Recovered`. The renderer
/// thread resumes GPU presentation on the next frame.
pub fn RendererRecovery::report_recovery_succeeded(
  self : RendererRecovery,
) -> Unit {
  match self.state {
    Recovering => {
      self.consecutive_failures = 0
      self.recovery_started_for_loss = false
      self.state = Recovered
    }
    _ => ()
  }
}

///|
/// Reports that GPU context + surface recreation failed. If the
/// consecutive failure count exceeds the budget, transitions to
/// `FallbackToRaster`; otherwise transitions back to `Lost` with the
/// original loss reason and a fresh deadline, allowing one retry per
/// ADR 0006.
pub fn RendererRecovery::report_recovery_failed(
  self : RendererRecovery,
  failure_reason : String,
  now_ms~ : Double,
) -> Unit {
  match self.state {
    Recovering => {
      self.recovery_started_for_loss = false
      self.consecutive_failures = self.consecutive_failures + 1
      if self.consecutive_failures >=
        self.budget.max_consecutive_recovery_failures {
        self.state = FallbackToRaster(failure_reason)
      } else {
        // Retry: a fresh deadline so the renderer thread gets another
        // 3-VSync budget for the next attempt.
        let deadline = self.budget.recovery_deadline_ms(now_ms)
        self.state = Lost(self.last_loss_reason, deadline)
      }
    }
    _ => ()
  }
}

///|
/// Transitions the state machine back to `Idle` from `Recovered`. Called by
/// the renderer thread once it has resumed GPU presentation. Calling from
/// any other state is a no-op.
pub fn RendererRecovery::acknowledge_recovered(self : RendererRecovery) -> Unit {
  match self.state {
    Recovered => self.state = Idle
    _ => ()
  }
}

///|
/// Returns a diagnostic snapshot suitable for inclusion in the mobile
/// runtime manifest (Phase 2.3). The snapshot is read-only; callers
/// should not mutate the recovery state based on this snapshot.
pub fn RendererRecovery::snapshot(
  self : RendererRecovery,
) -> RendererRecoverySnapshot {
  let state_label = match self.state {
    Idle => "idle"
    Lost(_, _) => "lost"
    Recovering => "recovering"
    Recovered => "recovered"
    FallbackToRaster(_) => "fallback-to-raster"
  }
  let deadline = match self.state {
    Lost(_, deadline) => deadline
    _ => 0.0
  }
  let fallback_reason = match self.state {
    FallbackToRaster(reason) => reason
    _ => ""
  }
  {
    state: state_label,
    loss_count: self.loss_count,
    last_loss_reason: self.last_loss_reason,
    last_loss_time_ms: self.last_loss_time_ms,
    recovery_deadline_ms: deadline,
    consecutive_failures: self.consecutive_failures,
    fallback_reason,
  }
}

///|
pub(all) struct RendererRecoverySnapshot {
  state : String
  loss_count : Int
  last_loss_reason : String
  last_loss_time_ms : Double
  recovery_deadline_ms : Double
  consecutive_failures : Int
  fallback_reason : String
} derive(Eq, Debug, ToJson)

///|
pub extend RendererRecoverySnapshot with Debug::{to_repr}

///|
pub extend RendererRecoverySnapshot with Eq::{not_equal, equal}

///|
pub extend RendererRecoverySnapshot with ToJson::{to_json}