import { getDb } from './db'

/**
 * IM 状态事件补偿（增量拉取）通用编排
 *
 * 各业务模块（好友、群、申请、读位置）共用一套 update_time + id 正向游标：
 * 从持久化游标出发循环拉取变更并合并进本地 store，直到某页不满（没有更多），
 * 用于 WebSocket 漏推后的兜底补偿（进入 IM、断线重连时触发）
 */

/** 增量拉取游标：上次拉到的位置 */
export interface PullCursor {
  lastUpdateTime?: number
  lastId?: number
}

/** 可作为游标的拉取记录：服务端按 update_time + id 返回，客户端取最后一条推进游标 */
interface PullRecord {
  id: number
  updateTime?: number
}

/** 单次拉取条数（与后端 limit 上限对齐） */
const PULL_PAGE_SIZE = 100
/** 单轮最多翻页数，兜底防御异常游标导致的死循环 */
const PULL_MAX_PAGES = 100
/** 状态事件拉取回扫窗口，覆盖同秒内旧行更新和客户端 / 服务端时钟精度差 */
const PULL_OVERLAP_MS = 5000
/** 消息类 minId 拉取的单轮翻页上限，兜底防御异常游标死翻；消息量可能远大于状态事件，放宽到 1000 */
const MIN_ID_PULL_MAX_PAGES = 1000

/** 读取某模块的拉取游标；无则返回空游标（首次拉全量） */
export async function getPullCursor(key: string): Promise<PullCursor> {
  return (await getDb().getSetting<PullCursor>(key)) ?? {}
}

/**
 * 通用增量拉取：从持久化游标出发，循环拉取并应用变更，直到某页不满
 *
 * @param cursorKey 游标在 settings 里的 key（每个模块一份）
 * @param fetchPage 按游标拉一页（调对应 pull 接口，返回 VO 列表）
 * @param apply 把一页变更合并进本地 store；返回 false 表示本页未完全落地（如账号已切、依赖资源补拉失败），
 *              此时不推进游标并终止本轮，避免游标越过未落地的记录、导致后续增量永久漏拉。可返回 Promise
 */
export async function runIncrementalPull<T extends PullRecord>(
  cursorKey: string,
  fetchPage: (params: { lastId?: number; lastUpdateTime?: number; limit: number }) => Promise<T[]>,
  apply: (records: T[]) => boolean | Promise<boolean>,
  isActive?: () => boolean
): Promise<void> {
  const storedCursor = await getPullCursor(cursorKey)
  const highWater = { ...storedCursor }
  let cursor =
    storedCursor.lastUpdateTime == null
      ? {}
      : { lastUpdateTime: Math.max(0, storedCursor.lastUpdateTime - PULL_OVERLAP_MS), lastId: 0 }
  for (let page = 0; page < PULL_MAX_PAGES; page++) {
    if (isActive && !isActive()) {
      return
    }
    const list = await fetchPage({
      lastUpdateTime: cursor.lastUpdateTime,
      lastId: cursor.lastId,
      limit: PULL_PAGE_SIZE
    })
    if (isActive && !isActive()) {
      return
    }
    if (list.length > 0) {
      // apply 未完全落地（返回 false）时直接终止：游标只能跟着已落地的数据走，否则会跳过本页记录
      if ((await apply(list)) === false) {
        return
      }
      if (isActive && !isActive()) {
        return
      }
      // 推进游标到本页最后一条并持久化：下次从这里接着拉
      const last = list[list.length - 1]
      if (!last) {
        return
      }
      if (last.updateTime == null) {
        return
      }
      cursor = { lastUpdateTime: last.updateTime, lastId: last.id }
      if (
        highWater.lastUpdateTime == null ||
        cursor.lastUpdateTime > highWater.lastUpdateTime ||
        (cursor.lastUpdateTime === highWater.lastUpdateTime &&
          cursor.lastId > (highWater.lastId ?? 0))
      ) {
        highWater.lastUpdateTime = cursor.lastUpdateTime
        highWater.lastId = cursor.lastId
        await getDb().setSetting(cursorKey, highWater)
      }
    }
    // 不满一页 = 没有更多变更
    if (list.length < PULL_PAGE_SIZE) {
      return
    }
  }
  console.warn(`[IM pull] ${cursorKey} 达到单轮翻页上限，提前结束本轮补偿`)
}

/**
 * 消息类增量拉取通用编排：按单调 minId 游标循环翻页，直到空页或游标不再前进
 *
 * 与 runIncrementalPull 的区别：消息游标是单调消息 id（非 update_time + id 复合游标），且 messageMaxId 的持久化
 * 跟随消息入库在 applyPage 内完成（messageStore），故本函数只负责「循环翻页机制」，不碰 settings，也不掺消息业务。
 *
 * @param initialMinId 起始游标：上次入库的最大消息 id
 * @param pageSize     单页条数
 * @param fetchPage    按 minId 拉一页
 * @param applyPage    处理本页（建会话 / 分发 / 入库 + 推进 messageMaxId）；返回 false 表示本页未落地，停止且不推进游标
 * @param isActive     每次 await 前后自检：取消 / 切账号时返回 false，丢弃本批不入库、不再翻页
 * @param maxPages     单轮翻页上限
 */
export async function runMinIdPull<T extends { id?: number }>(options: {
  applyPage: (records: T[], nextMinId?: number) => Promise<boolean> | Promise<void>
  fetchPage: (params: { minId: number; size: number }) => Promise<T[]>
  initialMinId: number
  isActive?: () => boolean
  maxPages?: number
  pageSize: number
}): Promise<void> {
  const { initialMinId, pageSize, fetchPage, applyPage, isActive } = options
  const maxPages = options.maxPages ?? MIN_ID_PULL_MAX_PAGES
  let minId = initialMinId || 0
  for (let page = 0; page < maxPages; page++) {
    if (isActive && !isActive()) {
      return
    }
    const list = await fetchPage({ minId, size: pageSize })
    // 拉取期间取消 / 切账号：丢弃本批不入库，也不再翻页
    if (isActive && !isActive()) {
      return
    }
    if (!list || list.length === 0) {
      return
    }
    // 本批最大消息 id 作为下次游标；无有效 id 则本批 apply 后停（无法继续翻页）
    const validIds = list.map((record) => record.id).filter((id): id is number => id != null)
    const nextMinId = validIds.length > 0 ? Math.max(...validIds) : undefined
    // applyPage 返回 false：本页未落地（如入库失败），不推进游标并终止，避免漏消息
    if ((await applyPage(list, nextMinId)) === false) {
      return
    }
    // 无有效 id，或游标没前进（后端契约是 id > minId，理论不会出现）：停，防御死翻
    if (nextMinId == null || nextMinId <= minId) {
      return
    }
    minId = nextMinId
  }
  console.warn('[IM pull] runMinIdPull 达到单轮翻页上限，提前结束本轮')
}
