import { LOG_PREFIX } from "../../shared/branding"
import type { AgentProvider } from "../../shared/types"

/** setTimeout's 32-bit delay ceiling; larger delays fire immediately otherwise. */
const MAX_TIMER_MS = 2_147_483_647

/** Default resume nudge — the session is mid-task, so "continue" is the right semantic. */
const RESUME_NUDGE = "Continue."

/** Narrow view of a chat the scheduler needs (subset of ChatRecord). */
export interface ResumeSchedulerChat {
  id: string
  provider: AgentProvider | null
  model?: string | null
  planMode: boolean
  autoContinueOnRateLimit?: boolean
  deletedAt?: number
}

export interface ResumeSchedulerStore {
  requireChat(chatId: string): ResumeSchedulerChat
  getQueuedTurn(chatId: string): unknown
}

export interface ResumeSchedulerCoordinator {
  activeTurns: { has(chatId: string): boolean }
  startTurnForChat(args: {
    chatId: string
    provider: AgentProvider
    content: string
    model: string
    effort?: string
    planMode: boolean
    appendUserPrompt: boolean
  }): Promise<void>
}

export interface RateLimitResumeSchedulerDeps {
  store: ResumeSchedulerStore
  coordinator: ResumeSchedulerCoordinator
  /** Injectable clock (tests). Defaults to Date.now. */
  now?: () => number
  /** Injectable timer (tests). Returns a cancel fn. Defaults to an unref'd setTimeout. */
  schedule?: (fn: () => void, delayMs: number) => () => void
  /** Resume nudge content. Defaults to "Continue.". */
  nudge?: string
}

/**
 * Server-side, in-memory timer that auto-resumes a rate-limited session once the
 * limit's resetAt passes — but only for chats that opted in
 * (`autoContinueOnRateLimit`). Resume = a "Continue." nudge with
 * `appendUserPrompt:false`, reusing the chat's persisted sessionToken via
 * RunnerProxy.startTurnForChat. No restart recovery: timers live only in memory,
 * so a server restart during the wait drops the pending resume.
 */
export class RateLimitResumeScheduler {
  private readonly store: ResumeSchedulerStore
  private readonly coordinator: ResumeSchedulerCoordinator
  private readonly now: () => number
  private readonly schedule: (fn: () => void, delayMs: number) => () => void
  private readonly nudge: string
  private readonly timers = new Map<string, () => void>()

  constructor(deps: RateLimitResumeSchedulerDeps) {
    this.store = deps.store
    this.coordinator = deps.coordinator
    this.now = deps.now ?? (() => Date.now())
    this.nudge = deps.nudge ?? RESUME_NUDGE
    this.schedule = deps.schedule ?? ((fn, delayMs) => {
      const handle = setTimeout(fn, delayMs)
      const maybeUnref = handle as unknown as { unref?: () => void }
      if (typeof maybeUnref.unref === "function") maybeUnref.unref()
      return () => clearTimeout(handle)
    })
  }

  /**
   * Arm (or re-arm) a resume timer for a chat that just hit a rate-limit.
   * `resetAtMs` is epoch milliseconds. No-op unless the chat opted in.
   */
  onRateLimit(chatId: string, resetAtMs: number): void {
    let chat: ResumeSchedulerChat
    try {
      chat = this.store.requireChat(chatId)
    } catch {
      return
    }
    if (chat.deletedAt) return
    if (!chat.autoContinueOnRateLimit) return
    if (!Number.isFinite(resetAtMs)) return

    this.cancel(chatId)
    const delayMs = Math.min(Math.max(0, resetAtMs - this.now()), MAX_TIMER_MS)
    const cancel = this.schedule(() => {
      this.timers.delete(chatId)
      void this.fire(chatId)
    }, delayMs)
    this.timers.set(chatId, cancel)
    console.log(LOG_PREFIX, `auto-continue armed chatId=${chatId} delayMs=${delayMs}`)
  }

  /** Cancel a pending resume (manual takeover, chat dispose, or disarm). */
  cancel(chatId: string): void {
    const cancel = this.timers.get(chatId)
    if (!cancel) return
    this.timers.delete(chatId)
    try { cancel() } catch { /* ignore */ }
  }

  cancelAll(): void {
    for (const chatId of [...this.timers.keys()]) this.cancel(chatId)
  }

  /** Visible for tests/diagnostics. */
  hasPending(chatId: string): boolean {
    return this.timers.has(chatId)
  }

  private async fire(chatId: string): Promise<void> {
    let chat: ResumeSchedulerChat
    try {
      chat = this.store.requireChat(chatId)
    } catch {
      return
    }
    // Re-check every gate at fire time — state can change during the wait.
    if (chat.deletedAt) return
    if (!chat.autoContinueOnRateLimit) return
    if (this.coordinator.activeTurns.has(chatId)) return
    if (this.store.getQueuedTurn(chatId)) return

    try {
      await this.coordinator.startTurnForChat({
        chatId,
        provider: chat.provider ?? "claude",
        content: this.nudge,
        model: chat.model ?? "sonnet",
        planMode: chat.planMode ?? false,
        appendUserPrompt: false,
      })
      console.log(LOG_PREFIX, `auto-continue resumed chatId=${chatId}`)
    } catch (err) {
      console.warn(
        LOG_PREFIX,
        `auto-continue resume failed chatId=${chatId}:`,
        err instanceof Error ? err.message : String(err),
      )
    }
  }
}
