// Tapestry write path. Per-instance MMKV queue, debounced fire-and-forget
// flush, gated on the per-instance Halflight `tapestry` flag. When the
// active account's instance does not advertise the flag, every `emit()`
// is a no-op and zero outbound traffic occurs.

import { AppState, type AppStateStatus } from 'react-native'
import { ContextForAccount } from 'src/lib/api-context'
import type { HalflightFeatures } from 'src/lib/features'
import { AccountStore } from 'src/state/accountStore'
import { appStorage, getFeatures } from 'src/state/storage'
import { isValidBody, type Letter, MAX_BODY_BYTES } from './grammar'

/**
 * Per-instance MMKV key the queue lives under. Exported so the read-side
 * selector (`useTapestryQueue` in `queue.ts`) can subscribe to the same key
 * without re-deriving the format string.
 */
export const queueKey = (host: string) => `tap.queue.${host}`

const QUEUE_KEY = queueKey

const FLUSH_INTERVAL_MS = 5_000
const FLUSH_AT_LENGTH = 50

const POST_PATH = '/api/v1/tapestry'

let flushTimer: ReturnType<typeof setTimeout> | null = null

function isTapestryEnabled(host: string): boolean {
  const features = getFeatures<HalflightFeatures>(host)
  return features?.tapestry === true
}

/**
 * Append a single letter to the active account's instance queue. No-op when
 * there is no active account or the instance does not advertise the
 * `tapestry` Halflight flag.
 */
export function emit(letter: Letter): void {
  const account = AccountStore.activeAccount()
  if (!account) return
  const host = account.instance
  if (!isTapestryEnabled(host)) return

  const key = QUEUE_KEY(host)
  const next = (appStorage.getString(key) ?? '') + letter
  appStorage.set(key, next)

  if (next.length >= FLUSH_AT_LENGTH) {
    void flushQueue(host, account.accountId)
    return
  }
  scheduleFlush(host, account.accountId)
}

function scheduleFlush(host: string, accountId: string): void {
  if (flushTimer) return
  flushTimer = setTimeout(() => {
    flushTimer = null
    void flushQueue(host, accountId)
  }, FLUSH_INTERVAL_MS)
}

async function flushQueue(host: string, accountId: string): Promise<void> {
  const key = QUEUE_KEY(host)
  const buffered = appStorage.getString(key) ?? ''
  if (buffered.length === 0) return

  // Steady-state queues never reach 256; the chunking guards a backlog
  // flush after long-running background drift, where the in-memory queue
  // grew past the per-POST cap before any flush could fire.
  const chunks: string[] = []
  for (let i = 0; i < buffered.length; i += MAX_BODY_BYTES) {
    chunks.push(buffered.slice(i, i + MAX_BODY_BYTES))
  }

  // Clear FIRST — fire-and-forget, drop on failure. The privacy posture
  // is "the server owns history, the device does not", so a failed flush
  // is preferred over an ever-growing local backlog.
  appStorage.remove(key)

  let ctx: ReturnType<typeof ContextForAccount>
  try {
    ctx = ContextForAccount(accountId)
  } catch {
    // Account was removed between queue append and flush; drop the chunks.
    return
  }

  for (const body of chunks) {
    if (!isValidBody(body)) continue
    ctx.postRaw(POST_PATH, body).catch(() => {})
  }
}

let appStateSubscribed = false

/**
 * Subscribe to `AppState` background transitions so the active account's
 * queue gets one final flush on the way out. Idempotent at module-load
 * time; safe to call from `_layout.tsx` startup alongside boot cleanup.
 */
export function attachAppStateListener(): void {
  if (appStateSubscribed) return
  appStateSubscribed = true
  AppState.addEventListener('change', (next: AppStateStatus) => {
    if (next !== 'background') return
    const account = AccountStore.activeAccount()
    if (!account) return
    if (!isTapestryEnabled(account.instance)) return
    void flushQueue(account.instance, account.accountId)
  })
}

let switchSubscribed = false

/**
 * Subscribe to user-facing account switches. The subscriber fires AFTER
 * `setActive`, so we walk every signed-in account and flush any non-empty
 * queue under the corresponding bearer — cheap because most are empty
 * most of the time, and avoids needing to capture the previous active id
 * out of band. Internal silent `setActive` calls (login completion,
 * auth-failure fallback) do not fire `subscribeSwitch` and so do not
 * trigger a flush — neither path has letters worth saving.
 */
export function attachAccountSwitchListener(): void {
  if (switchSubscribed) return
  switchSubscribed = true
  AccountStore.subscribeSwitch(() => {
    for (const a of AccountStore.list()) {
      void flushQueue(a.instance, a.accountId)
    }
  })
}
