// Holistic Pass 4 Phase 3.1 — per-account local mirror of the notifications
// marker. The server-side `/api/v1/markers?timeline[]=notifications` is the
// canonical "lastReadId" record on both Pixelfed and Mastodon, but neither
// server exposes a per-row read flag — the only way to render a per-row
// "new" indicator (Phase 3.4) is to derive `id > localMarker.lastReadId`
// against a local mirror. Mirrors the iOS Mastodon client's `MastodonFeedLoader`
// `isUnread(identifier)` shape.
//
// Per-account by accountId so switching accounts re-renders every consuming
// hook against a fresh marker. Snapshot caching is per-account so
// `useSyncExternalStore` consumers see stable references between renders —
// the same atomic-snapshot discipline Phase 2.1 lifted into AccountStore.

import { useSyncExternalStore } from 'react'
import { setNotificationMarker } from 'src/lib/api/shared'
import { AccountStore } from 'src/state/accountStore'
import { appKeys, appStorage } from 'src/state/storage'

export type LocalMarker = { lastReadId: string; updatedAt: number }

// Stable references per accountId — getActiveSnapshot() returns the cached
// object until advanceMarker() rebuilds it for that account. JSON.parse on
// every read would mint a fresh object each call and trip the
// `useSyncExternalStore` "result of getSnapshot should be cached" guardrail.
const snapshots = new Map<string, LocalMarker | null>()

const listeners = new Set<() => void>()

function readMarker(accountId: string): LocalMarker | null {
  const raw = appStorage.getString(appKeys.notificationsLocalMarker(accountId))
  if (!raw) return null
  try {
    const parsed = JSON.parse(raw) as LocalMarker
    if (typeof parsed?.lastReadId === 'string' && typeof parsed?.updatedAt === 'number') {
      return parsed
    }
    return null
  } catch {
    return null
  }
}

function ensureSnapshot(accountId: string): LocalMarker | null {
  if (!snapshots.has(accountId)) snapshots.set(accountId, readMarker(accountId))
  return snapshots.get(accountId) ?? null
}

function rebuildSnapshot(accountId: string): void {
  snapshots.set(accountId, readMarker(accountId))
}

function emit(): void {
  for (const fn of listeners) fn()
}

// Both Pixelfed and Mastodon issue numerical-string ids in chronological
// order. BigInt parse is exact across id-length changes (snowflake roll-overs,
// Pixelfed's 64-bit ids); fall back to lexical compare if a non-numeric id
// ever leaks through (defensive — should not happen on either server today).
function idIsNewer(a: string, b: string): boolean {
  try {
    return BigInt(a) > BigInt(b)
  } catch {
    return a > b
  }
}

export function getLocalMarker(accountId: string): LocalMarker | null {
  return ensureSnapshot(accountId)
}

export function isUnread(notificationId: string, marker: LocalMarker | null): boolean {
  if (!marker) return true
  return idIsNewer(notificationId, marker.lastReadId)
}

// --- Server-write coalescing ---------------------------------------------
//
// Tap-to-mark-read (Phase 3.4) advances the local mirror immediately for
// instant visual feedback, then schedules a debounced server-side write so
// a flurry of taps coalesces to one round-trip. The pending id always
// monotonically increases (clamp-forward) so a tap on an older row after a
// newer one doesn't downgrade the pending write.

const SERVER_WRITE_DEBOUNCE_MS = 800

let pendingServerWriteId: string | null = null
let pendingServerWriteAccountId: string | null = null
let serverWriteTimer: ReturnType<typeof setTimeout> | null = null

function scheduleServerWrite(accountId: string, id: string): void {
  // If we already have a pending write for a different account (the user
  // switched mid-debounce), flush whatever's pending immediately so it lands
  // against the right server. This is rare in practice — the debounce window
  // is 800 ms — but the right defensive shape.
  if (pendingServerWriteAccountId && pendingServerWriteAccountId !== accountId) {
    flushServerWriteNow()
  }
  if (
    pendingServerWriteId &&
    pendingServerWriteAccountId === accountId &&
    !idIsNewer(id, pendingServerWriteId)
  ) {
    return
  }
  pendingServerWriteId = id
  pendingServerWriteAccountId = accountId
  if (serverWriteTimer) clearTimeout(serverWriteTimer)
  serverWriteTimer = setTimeout(flushServerWriteNow, SERVER_WRITE_DEBOUNCE_MS)
}

function flushServerWriteNow(): void {
  if (serverWriteTimer) {
    clearTimeout(serverWriteTimer)
    serverWriteTimer = null
  }
  const id = pendingServerWriteId
  const writeAccountId = pendingServerWriteAccountId
  pendingServerWriteId = null
  pendingServerWriteAccountId = null
  if (!id || !writeAccountId) return
  // Only flush if this account is still active — if the user switched in the
  // tail of the debounce window, the marker write would target the wrong
  // server. The local mirror already wrote through against `writeAccountId`,
  // so the next focus on that account will retry the server write.
  if (AccountStore.activeId() !== writeAccountId) return
  setNotificationMarker(id).catch(() => {
    // Transient — next focus / next tap will retry.
  })
}

/**
 * Tap-to-mark-read entry point. Advances the local marker forward (clamp —
 * never moves backward) for the active account and schedules a debounced
 * server-side write. Synchronous return so the row's tap handler can fire
 * navigation immediately afterwards.
 */
export function markNotificationReadOnTap(notificationId: string): void {
  const accountId = AccountStore.activeId()
  if (!accountId) return
  const current = ensureSnapshot(accountId)
  if (current && !idIsNewer(notificationId, current.lastReadId)) return
  appStorage.set(
    appKeys.notificationsLocalMarker(accountId),
    JSON.stringify({ lastReadId: notificationId, updatedAt: Date.now() })
  )
  rebuildSnapshot(accountId)
  emit()
  scheduleServerWrite(accountId, notificationId)
}

/**
 * Focus auto-advance entry point. Advances both the local mirror and the
 * server marker to the unfiltered newest id (Phase 3.2) — fires once per
 * focus event so the debounce isn't required. Awaits the server write so
 * the caller can chain unread-count cache invalidation in the same tick.
 */
export async function advanceMarkerOnFocus(notificationId: string): Promise<void> {
  const accountId = AccountStore.activeId()
  if (!accountId) return
  const current = ensureSnapshot(accountId)
  const advanced = !current || idIsNewer(notificationId, current.lastReadId)
  if (advanced) {
    appStorage.set(
      appKeys.notificationsLocalMarker(accountId),
      JSON.stringify({ lastReadId: notificationId, updatedAt: Date.now() })
    )
    rebuildSnapshot(accountId)
    emit()
  }
  // The server write happens regardless of the local-mirror clamp result —
  // a user signing in on a fresh device may have a server marker behind the
  // local one (rare but possible). The server endpoint is idempotent.
  await setNotificationMarker(notificationId)
}

// --- useSyncExternalStore adapter ---------------------------------------

export const localMarkerStore = {
  /**
   * Subscribe to local marker changes. Re-renders consumers when the marker
   * advances *or* when the active account flips (so a newly-active account's
   * marker is read fresh from the snapshot map).
   */
  subscribe(fn: () => void): () => void {
    listeners.add(fn)
    const unsubAccount = AccountStore.subscribe(fn)
    return () => {
      listeners.delete(fn)
      unsubAccount()
    }
  },

  /**
   * Stable snapshot of the active account's local marker. Returns `null`
   * when no account is active or no marker has been written yet. Stable
   * reference between calls — consumers feed this directly into
   * useSyncExternalStore.
   */
  getActiveSnapshot(): LocalMarker | null {
    const id = AccountStore.activeId()
    if (!id) return null
    return ensureSnapshot(id)
  },
} as const

/**
 * Re-renders the consumer whenever the local marker for the active account
 * advances. Returns `null` until the first marker is written.
 */
export function useActiveLocalMarker(): LocalMarker | null {
  return useSyncExternalStore(
    localMarkerStore.subscribe,
    localMarkerStore.getActiveSnapshot,
    localMarkerStore.getActiveSnapshot
  )
}
