///|
/// resync:两端世界分叉之后,用一份完整状态把落后的一端掰回来。
///
///   moon run examples/resync
///
/// V0.9 之前只能「发现」分叉:确认帧哈希对不上就报出来,然后两端各算各的。
/// 这一版多了一条补救路径:
///
/// 1. 对端在某个确认帧上的校验和对不上 → 记下这一帧;
/// 2. 向对端要这一帧的**完整状态**(`SnapRequest` → `Snap`);
/// 3. `resync_from` 把本端 restore 到那一帧,再用本端输入环重放到当前帧。
///
/// 为什么必须「整段替换」而不是「回滚」:回滚是从**本端**快照恢复的,而快照
/// 里就是那份错的世界——回滚只会把同一个错误再算一遍,深度多大都没用。
///
/// 为什么恢复点必须落在回滚窗口内:`frame` 之后的帧要重放,而重放靠的是输入
/// 环;更早的输入已经被环淘汰,重放会用「猜出来」的值,等于换一个地方重新
/// 分叉。
const TOTAL : Int32 = 400

///|
/// 事故帧:1 号在这一帧「本地执行 A、线上发 B」。
///
/// 只改本端内存里的状态是没用的——回滚会从快照恢复并重放,任何没进输入流的
/// 改动都会被洗掉;只有输入流本身出错才会留下永久痕迹。
const CORRUPT : Int32 = 100

///|
/// 每帧重发最近多少帧的输入(扛丢包)。
const RESEND : Int32 = 8

///|
/// 请求失败时的重试间隔:`SnapRequest` / `Snap` 本身也可能丢。
const ASK_EVERY : Int32 = 6

///|
const WINDOW : Int32 = 128

///|
/// 一局的重同步过程记录。
pub(all) struct ResyncStats {
  /// 两端最终的确认帧。
  p0_confirmed : Int32
  p1_confirmed : Int32
  /// P0 首次检出失配的确认帧;0 表示全程没失配。
  detected : Int32
  /// P0 实际完成的恢复点;0 表示没恢复。
  repaired : Int32
  /// P0 发出的 `SnapRequest` 个数。
  asked : Int32
  /// P0 成功重同步的次数。
  resyncs : Int32
  /// 恢复前检出的失配次数。
  mismatch_before : Int32
  /// 恢复后检出的失配次数(应该恒为 0)。
  mismatch_after : Int32
  /// 恢复之后的首个分叉帧;0 表示从恢复点起逐帧一致。
  bad_after : Int32
  /// `Snap` 里状态字节的长度。
  state_bytes : Int32
  /// 两端各自的回滚次数。
  p0_rollbacks : Int32
  p1_rollbacks : Int32
  /// 链路丢掉的包数。
  dropped : Int32
}

///|
/// 恢复过程的中间状态。放在一个可变结构里,是因为它同时被三条路径改写:
/// 收包(检测 / 应用状态)、发包(请求 / 应答)、收尾(继续重试)。
struct Healing {
  /// 待请求的恢复点;0 表示不需要。
  mut need : Int32
  /// 上一次发出请求的帧。
  mut last_ask : Int32
  /// 发出的请求数。
  mut asked : Int32
  /// 已完成的恢复点;0 表示还没恢复。
  mut repaired : Int32
  /// 首个失配的确认帧。
  mut detected : Int32
  /// 恢复前 / 恢复后的失配次数。
  mut mismatch_before : Int32
  mut mismatch_after : Int32
  /// 恢复之后已逐帧比对到哪一帧。
  mut checked : Int32
  /// 恢复之后的首个分叉帧。
  mut bad_after : Int32
  /// 需要回一条 `Snap` 的帧;0 表示没有。
  mut reply : Int32
  /// 最近一次收到的状态字节数。
  mut state_bytes : Int32
}

///|
fn min32(a : Int32, b : Int32) -> Int32 {
  if a < b {
    a
  } else {
    b
  }
}

///|
/// 一帧上行包:最近 `RESEND` 帧的输入 + 确认帧 + 当前确认帧的校验和。
///
/// `corrupt` 不为 0 时,那一帧的输入会被改掉——模拟「本地算了 A,序列化出去
/// 的却是 B」。
fn upstream(
  s : @session.Session[@scene.Scene],
  id : Int32,
  upto : Int32,
  corrupt : Int32,
) -> Array[@protocol.Message] {
  let msgs : Array[@protocol.Message] = []
  let mut m = upto - RESEND + 1
  if m < 1 {
    m = 1
  }
  while m <= upto {
    let mut input = @scene.scripted_input(m, id)
    if m == corrupt {
      input = input ^ 1UL
    }
    msgs.push(
      @protocol.Message::Input(@protocol.InputFrame::new_from(id, m, [input])),
    )
    m = m + 1
  }
  msgs.push(
    @protocol.Message::Confirm(@protocol.ConfirmFrame::new(s.confirmed())),
  )
  match s.hash_at(s.confirmed()) {
    Some(h) =>
      msgs.push(
        @protocol.Message::Checksum(
          @protocol.ChecksumFrame::new(s.confirmed(), h),
        ),
      )
    None => ()
  }
  msgs
}

///|
/// 投递到落后的 P0:输入 / 确认 / 校验和 / 完整状态。
///
/// 校验和只在**本端也已确认**的帧上比对:尚未确认的帧里还混着预测输入,
/// 和对方的真实输入本来就不同,比了只会得到假分叉。
fn apply_to_laggard(
  payload : Bytes,
  s : @session.Session[@scene.Scene],
  heal : Healing,
) -> Unit {
  for msg in @protocol.decode_batch(payload) {
    match msg {
      @protocol.Input(x) => {
        let mut i = 0
        for bits in x.inputs {
          s.receive_from(x.player + i, x.frame, bits)
          i = i + 1
        }
      }
      @protocol.Checksum(c) =>
        if c.frame <= s.confirmed() {
          match s.hash_at(c.frame) {
            Some(mine) =>
              if mine != c.hash {
                if heal.repaired == 0 {
                  heal.mismatch_before = heal.mismatch_before + 1
                } else {
                  heal.mismatch_after = heal.mismatch_after + 1
                }
                // 第一次失配的确认帧就是最合适的恢复点:它一定不早于事故帧,
                // 之后的输入在两端已经一致,重放不会再分叉。
                if heal.detected == 0 {
                  heal.detected = c.frame
                  heal.need = c.frame
                }
              }
            None => ()
          }
        }
      @protocol.Snap(snap) =>
        if heal.repaired == 0 && heal.need != 0 && snap.frame == heal.need {
          if s.resync_from(snap.frame, snap.state) {
            heal.repaired = snap.frame
            heal.need = 0
            heal.checked = snap.frame
            heal.state_bytes = snap.state.length()
          }
        }
      // 只要状态:应答由 `resync_from` 应用,别的消息与本端无关
      _ => ()
    }
  }
}

///|
/// 投递到权威端 P1:输入 / 状态请求。
///
/// P1 不比对校验和:两人局里双方同时「互相纠正」会来回覆盖。真实系统里谁算
/// 权威要么按房主定,要么按多数票——这是上层策略,库只提供「取状态」和
/// 「换状态」两个动作。
fn apply_to_authority(
  payload : Bytes,
  s : @session.Session[@scene.Scene],
  heal : Healing,
) -> Unit {
  for msg in @protocol.decode_batch(payload) {
    match msg {
      @protocol.Input(x) => {
        let mut i = 0
        for bits in x.inputs {
          s.receive_from(x.player + i, x.frame, bits)
          i = i + 1
        }
      }
      // 每个请求都回一次:丢包时靠调用方的定时重试收敛
      @protocol.SnapRequest(r) => heal.reply = r.frame
      _ => ()
    }
  }
}

///|
/// 恢复点之后逐帧比对两端在共同确认帧上的哈希。
///
/// 边跑边比,而不是最后一次性扫:历史哈希只保留最近一个窗口,最后再扫时
/// 早先的帧已经取不到了。
fn check_synced(
  p0 : @session.Session[@scene.Scene],
  p1 : @session.Session[@scene.Scene],
  heal : Healing,
) -> Unit {
  if heal.repaired == 0 {
    return
  }
  let c = min32(p0.confirmed(), p1.confirmed())
  let mut f = heal.checked + 1
  while f <= c {
    match (p0.hash_at(f), p1.hash_at(f)) {
      (Some(a), Some(b)) =>
        if a != b && heal.bad_after == 0 {
          heal.bad_after = f
        }
      _ => ()
    }
    f = f + 1
  }
  heal.checked = c
}

///|
/// 跑一局:`corrupt` 不为 0 时,P1 在那一帧发出与本地执行不一致的输入。
pub fn run(
  spec : @scene.LinkSpec,
  total : Int32,
  corrupt : Int32,
) -> ResyncStats {
  let p0 = @session.Session::with_window(@scene.Scene::new(), 0, WINDOW)
  let p1 = @session.Session::with_window(@scene.Scene::new(), 1, WINDOW)
  let net01 = @sim_net.NetSim::new(
    @sim_net.Link::new(
      spec.delay,
      spec.jitter,
      spec.loss,
      spec.reorder,
      20261028UL,
    ),
  )
  let net10 = @sim_net.NetSim::new(
    @sim_net.Link::new(
      spec.delay,
      spec.jitter,
      spec.loss,
      spec.reorder,
      424242UL,
    ),
  )
  let heal : Healing = {
    need: 0,
    last_ask: 0,
    asked: 0,
    repaired: 0,
    detected: 0,
    mismatch_before: 0,
    mismatch_after: 0,
    checked: 0,
    bad_after: 0,
    reply: 0,
    state_bytes: 0,
  }
  let mut f = 0
  while f < total {
    f = f + 1
    ignore(p0.advance(@scene.scripted_input(f, 0)))
    ignore(p1.advance(@scene.scripted_input(f, 1)))
    let msgs0 = upstream(p0, 0, f, 0)
    if heal.need != 0 && f - heal.last_ask >= ASK_EVERY {
      msgs0.push(
        @protocol.Message::SnapRequest(
          @protocol.SnapRequestFrame::new(heal.need),
        ),
      )
      heal.last_ask = f
      heal.asked = heal.asked + 1
    }
    net01.send(f, @protocol.encode_batch(msgs0))
    let msgs1 = upstream(p1, 1, f, corrupt)
    if heal.reply != 0 {
      match p1.export_state(heal.reply) {
        Some(state) =>
          msgs1.push(
            @protocol.Message::Snap(@protocol.SnapFrame::new(heal.reply, state)),
          )
        None => ()
      }
      heal.reply = 0
    }
    net10.send(f, @protocol.encode_batch(msgs1))
    for payload in net10.recv(f) {
      apply_to_laggard(payload, p0, heal)
    }
    for payload in net01.recv(f) {
      apply_to_authority(payload, p1, heal)
    }
    check_synced(p0, p1, heal)
  }
  // 收尾:不再推进新帧,但继续重发最后一段输入、继续重试状态请求,
  // 直到两端的确认帧都追平——否则丢掉的包永远没有第二次机会。
  let mut t = total
  while t < total + 200 && (p0.confirmed() < total || p1.confirmed() < total) {
    t = t + 1
    let msgs0 = upstream(p0, 0, total, 0)
    if heal.need != 0 && t - heal.last_ask >= ASK_EVERY {
      msgs0.push(
        @protocol.Message::SnapRequest(
          @protocol.SnapRequestFrame::new(heal.need),
        ),
      )
      heal.last_ask = t
      heal.asked = heal.asked + 1
    }
    net01.send(t, @protocol.encode_batch(msgs0))
    let msgs1 = upstream(p1, 1, total, corrupt)
    if heal.reply != 0 {
      match p1.export_state(heal.reply) {
        Some(state) =>
          msgs1.push(
            @protocol.Message::Snap(@protocol.SnapFrame::new(heal.reply, state)),
          )
        None => ()
      }
      heal.reply = 0
    }
    net10.send(t, @protocol.encode_batch(msgs1))
    for payload in net10.recv(t) {
      apply_to_laggard(payload, p0, heal)
    }
    for payload in net01.recv(t) {
      apply_to_authority(payload, p1, heal)
    }
    check_synced(p0, p1, heal)
  }
  {
    p0_confirmed: p0.confirmed(),
    p1_confirmed: p1.confirmed(),
    detected: heal.detected,
    repaired: heal.repaired,
    asked: heal.asked,
    resyncs: p0.resyncs(),
    mismatch_before: heal.mismatch_before,
    mismatch_after: heal.mismatch_after,
    bad_after: heal.bad_after,
    state_bytes: heal.state_bytes,
    p0_rollbacks: p0.rollbacks(),
    p1_rollbacks: p1.rollbacks(),
    dropped: net01.dropped() + net10.dropped(),
  }
}

///|
fn main {
  let spec = @scene.LinkSpec::new(3, 2, 4, 8)
  let stats = run(spec, TOTAL, CORRUPT)
  println("resync:分叉之后用完整状态把落后者掰回来")
  println("")
  println(
    "链路:延迟 \{spec.delay} 帧 + 抖动 \{spec.jitter} 帧 + 丢包 \{spec.loss}% + 乱序 \{spec.reorder}%",
  )
  println(
    "事故:1 号在第 \{CORRUPT} 帧本地执行 A、线上发 B(输入序列化错位)",
  )
  println("      两端的输入流从此不同,靠回滚永远追不回来")
  println("")
  if stats.detected == 0 {
    println("没有检出失配:这一局本来就一致")
  } else {
    println(
      "P0 首次失配:确认帧 \{stats.detected} 的状态哈希不同(共 \{stats.mismatch_before} 次失配)",
    )
    println(
      "P0 请求第 \{stats.detected} 帧的完整状态:发出 \{stats.asked} 个 SnapRequest",
    )
    println(
      "P0 已恢复:restore 到第 \{stats.repaired} 帧(状态 \{stats.state_bytes} 字节),再用输入环重放",
    )
  }
  println("")
  println(
    "确认帧:P0 \{stats.p0_confirmed} / P1 \{stats.p1_confirmed};回滚 P0 \{stats.p0_rollbacks} / P1 \{stats.p1_rollbacks}",
  )
  println(
    "重同步次数:P0 \{stats.resyncs};恢复后失配 \{stats.mismatch_after} 次",
  )
  if stats.bad_after == 0 {
    println(
      "恢复后:从恢复点到结束,两端在共同确认帧上逐帧哈希一致",
    )
  } else {
    println("恢复后仍然在第 \{stats.bad_after} 帧分叉")
  }
  println(
    "注:恢复点之前的旧哈希留在落后者本地——重同步修的是未来,不改写历史。",
  )
}