import * as SecureStore from 'expo-secure-store'
import { createMMKV } from 'react-native-mmkv'

// --- Tier 1: Secrets (Keychain / Keystore) ---
//
// Per-account tokens and per-instance OAuth app credentials. There is no
// "current" slot — every secret is keyed by accountId or instance hostname.

const secureKeyFor = {
  accessToken: (accountId: string) => `secure.account.${accountId}.access_token`,
  refreshToken: (accountId: string) => `secure.account.${accountId}.refresh_token`,
  clientId: (instance: string) => `secure.instance.${instance}.client_id`,
  clientSecret: (instance: string) => `secure.instance.${instance}.client_secret`,
}

// iOS Keychain accessibility for every write. WHEN_PASSCODE_SET_THIS_DEVICE_ONLY
// upgrades the default kSecAttrAccessibleWhenUnlocked posture in three ways:
//   1. Items disappear when the user removes their device passcode (defends
//      against the "passcode cleared via Face ID bypass" niche).
//   2. Items are NOT migrated to a new device via iCloud Keychain — a phone
//      resold or stolen-and-restored from backup never carries our tokens.
//   3. On A7+ hardware, items are bound to the Secure Enclave.
// Reads still work whenever the device is unlocked, which is every code path
// the app exercises (cold-start verify, app-foreground refresh, every
// foregrounded API call). `requireAuthentication` is intentionally NOT set —
// it would block the cold-start verify path and notification-tap routing
// before the user has ever seen a screen.
//
// Android: `keychainAccessible` is iOS-only; expo-secure-store's Android
// backend is the OEM Keystore, which already gates reads on user
// authentication when `requireAuthentication` is set, and otherwise binds
// keys to the device. Passing the option does no harm on Android.
const SECURE_OPTS: SecureStore.SecureStoreOptions = {
  keychainAccessible: SecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY,
}

export const secureStore = {
  getAccessToken: (accountId: string) =>
    SecureStore.getItem(secureKeyFor.accessToken(accountId), SECURE_OPTS),
  setAccessToken: (accountId: string, v: string) =>
    SecureStore.setItem(secureKeyFor.accessToken(accountId), v, SECURE_OPTS),
  deleteAccessToken: (accountId: string) =>
    SecureStore.deleteItemAsync(secureKeyFor.accessToken(accountId), SECURE_OPTS),

  getRefreshToken: (accountId: string) =>
    SecureStore.getItem(secureKeyFor.refreshToken(accountId), SECURE_OPTS),
  setRefreshToken: (accountId: string, v: string) =>
    SecureStore.setItem(secureKeyFor.refreshToken(accountId), v, SECURE_OPTS),
  deleteRefreshToken: (accountId: string) =>
    SecureStore.deleteItemAsync(secureKeyFor.refreshToken(accountId), SECURE_OPTS),

  getClientId: (instance: string) =>
    SecureStore.getItem(secureKeyFor.clientId(instance), SECURE_OPTS),
  setClientId: (instance: string, v: string) =>
    SecureStore.setItem(secureKeyFor.clientId(instance), v, SECURE_OPTS),
  deleteClientId: (instance: string) =>
    SecureStore.deleteItemAsync(secureKeyFor.clientId(instance), SECURE_OPTS),

  getClientSecret: (instance: string) =>
    SecureStore.getItem(secureKeyFor.clientSecret(instance), SECURE_OPTS),
  setClientSecret: (instance: string, v: string) =>
    SecureStore.setItem(secureKeyFor.clientSecret(instance), v, SECURE_OPTS),
  deleteClientSecret: (instance: string) =>
    SecureStore.deleteItemAsync(secureKeyFor.clientSecret(instance), SECURE_OPTS),

  /**
   * Wipe every secret on the device. Reserved for the "Sign out of all"
   * action — per-account remove uses targeted delete*() calls so account A
   * can't accidentally take account B's tokens with it.
   */
  clearAllAccounts: async (accountIds: string[], instances: string[]) => {
    await Promise.all([
      ...accountIds.flatMap((id) => [
        SecureStore.deleteItemAsync(secureKeyFor.accessToken(id)),
        SecureStore.deleteItemAsync(secureKeyFor.refreshToken(id)),
      ]),
      ...instances.flatMap((host) => [
        SecureStore.deleteItemAsync(secureKeyFor.clientId(host)),
        SecureStore.deleteItemAsync(secureKeyFor.clientSecret(host)),
      ]),
    ])
  },
} as const

// --- Tier 2: User-specific cache (per-account) ---

export const userStorage = createMMKV({ id: 'user' })

export const userKeys = {
  profile: (accountId: string) => `user.account.${accountId}.profile`,
  credentialsVerifiedAt: (accountId: string) =>
    `user.account.${accountId}.credentials_verified_at`,
} as const

export function clearUserKeysForAccount(accountId: string): void {
  const prefix = `user.account.${accountId}.`
  for (const k of userStorage.getAllKeys()) {
    if (k.startsWith(prefix)) userStorage.remove(k)
  }
}

// --- Tier 3: App-level (persists across logout) ---

export const appStorage = createMMKV({ id: 'app', compareBeforeSet: true })

export const appKeys = {
  features: 'app.features',
  // Per-instance escape hatch: set to true after a 404 on the RSVP path so
  // the UI stops offering RSVP even if nodeinfo still claims the flag is on.
  rsvpMissing: (instance: string) => `app.rsvpMissing.${instance}` as const,
  // Android prominent-disclosure (D.2): one boolean per permission kind so
  // the in-app explanation modal fires once per device, not every time. iOS
  // uses the NS*UsageDescription strings instead and never reads these.
  permissionDisclosed: (kind: 'camera' | 'microphone') =>
    `app.permissions.disclosed.${kind}` as const,
  // Acceptable Use first-touch timestamp. Set on the first composer mount
  // per install; once present, the non-blocking toast (see
  // `useAcceptableUseToastOnce`) never re-fires. Existing installs that saw
  // the earlier blocking AlertDialog already have this set, so they are not
  // re-prompted with the toast.
  acceptableUseAcceptedAt: 'app.acceptableUseAcceptedAt',
  // Mastodon Pass §3.3: per-(accountType, host) capability cache. Stored as
  // JSON `InstanceCapabilities` blobs with a 24 h TTL (the read-side checks
  // `fetchedAt`). Keyed by `accountType` first so Pixelfed and Mastodon
  // entries on the same host (rare but possible across federation) never
  // collide, and so the type-shaped defaults fall out cleanly when no entry
  // exists yet.
  capabilities: (accountType: string, instance: string) =>
    `app.capabilities.${accountType}.${instance}` as const,
  // Holistic Pass 4 Phase 3.1: per-account local mirror of the server-side
  // notifications marker. JSON-encoded `{ lastReadId, updatedAt }`. The local
  // mirror writes through synchronously on every tap (so the per-row "new"
  // indicator clears instantly) while the server-side `setNotificationMarker`
  // writeback is debounced — see `src/lib/notifications/marker.ts`.
  notificationsLocalMarker: (accountId: string) =>
    `notifications.localMarker.${accountId}` as const,
} as const

// Features are keyed per instance so accounts on different servers never see
// stale flags. Stored as `app.features.<instance>` → JSON blob. Accounts on
// the same instance correctly share these — features encode server
// capability, not user state.
export function featureKeyFor(instance: string): string {
  return `${appKeys.features}.${instance}`
}

export function setFeatures(instance: string, features: Record<string, boolean>): void {
  appStorage.set(featureKeyFor(instance), JSON.stringify(features))
}

export function getFeatures<T extends Record<string, boolean> = Record<string, boolean>>(
  instance: string
): T | null {
  const raw = appStorage.getString(featureKeyFor(instance))
  if (!raw) return null
  try {
    return JSON.parse(raw) as T
  } catch {
    return null
  }
}

export const uiKeys = {
  theme: 'ui.theme',
  forceSensitive: 'ui.forceSensitive',
  hideCaptions: 'ui.hideCaptions',
  hideStories: 'ui.hideStories',
  showAltText: 'ui.showAltText',
  requireSelfAltText: 'ui.requireSelfAltText',
  mediaEdit: 'ui.mediaEdit',
  storiesOnboardingCompleted: 'ui+xs.v0.1.2.stories_onboarding_completed',
  homeLens: 'ui.homeLens',
  // Legacy global key kept readable for one-time fallback in
  // `getHomeMastodonList`. New writes go through the per-account key below.
  homeMastodonListLegacy: 'ui.home.mastodonList',
  // Holistic Pass 4 Phase 5.3: per-account selected list filter on the home
  // tab. Migrated from the legacy global `ui.home.mastodonList` so switching
  // between two Mastodon accounts no longer cross-contaminates picks.
  homeMastodonList: (accountId: string) => `ui.home.mastodonList.${accountId}` as const,
  // Holistic Pass 4 Phase 5.3: per-account preferred list ordering on the
  // home filter picker and Settings → Lists. JSON `string[]` of list ids;
  // missing-from-server ids drop out at apply time, new server-side lists
  // append at the bottom.
  listsOrder: (accountId: string) => `ui.lists.order.${accountId}` as const,
} as const

// --- Account registry ---
//
// MMKV instance dedicated to the account list and active-id pointer. Stays
// separate from `app`/`user` so the schema is easier to reason about and so
// `compareBeforeSet` semantics apply only here (the active-id setter relies
// on listeners *not* firing on equal-value writes).

export const accountsStorage = createMMKV({ id: 'accounts', compareBeforeSet: true })

export const accountKeys = {
  list: 'accounts.list', // JSON: AccountSummary[]
  activeId: 'accounts.activeId', // string | undefined
} as const

// User-selectable themes. `system` defers to the OS scheme; the resolver
// lives in _layout.tsx. Stored values outside this set fall back to `system`.
export const VALID_THEMES = new Set(['system', 'light', 'dark'])

export type ThemeChoice = 'system' | 'light' | 'dark'

/**
 * Single writer for the active theme preference. Returns true when the
 * stored value actually changed, so callers can gate side-effects (haptic,
 * Tapestry `T` emit) on real user-driven changes rather than no-op writes.
 * `useMMKVString(uiKeys.theme, appStorage)` consumers still re-render via
 * the underlying MMKV listener — this helper is just sugar over
 * `appStorage.set(uiKeys.theme, …)` with an equality short-circuit.
 *
 * The Tapestry `T` letter is emitted at the call sites (Appearance settings,
 * the login-screen dark-mode toggle) rather than here, to keep `storage.ts`
 * free of a dep on `src/lib/tapestry/emit`, which itself imports from this
 * file (`appStorage`, `getFeatures`, `AccountStore`).
 */
export function setTheme(value: ThemeChoice): boolean {
  if (appStorage.getString(uiKeys.theme) === value) return false
  appStorage.set(uiKeys.theme, value)
  return true
}

export function cleanupStaleThemeIfNeeded() {
  const stored = appStorage.getString(uiKeys.theme)
  if (!stored || VALID_THEMES.has(stored)) return
  appStorage.set(uiKeys.theme, 'system')
}

// --- Mastodon Lists (Holistic Pass 4 Phase 5.3) -------------------------
//
// `getListOrder` / `setListOrder` persist the user's preferred ordering of
// their Mastodon lists per-account. The stored value is a JSON `string[]` of
// list ids; consumers compose it with the server-side `getMastodonLists()`
// result via `applyListOrder` so removed-server-side lists drop out and new
// ones append.
//
// `getHomeMastodonList` / `setHomeMastodonList` persist the home tab's
// selected list filter per-account. On first read where the per-account key
// is unset, the legacy global `ui.home.mastodonList` value is read once as a
// fallback so the user's existing pick survives the upgrade.

export function getListOrder(accountId: string): string[] {
  const raw = appStorage.getString(uiKeys.listsOrder(accountId))
  if (!raw) return []
  try {
    const parsed = JSON.parse(raw)
    return Array.isArray(parsed)
      ? (parsed as string[]).filter((s) => typeof s === 'string')
      : []
  } catch {
    return []
  }
}

export function setListOrder(accountId: string, ids: string[]): void {
  appStorage.set(uiKeys.listsOrder(accountId), JSON.stringify(ids))
}

export function getHomeMastodonList(accountId: string): string {
  const v = appStorage.getString(uiKeys.homeMastodonList(accountId))
  if (v) return v
  // One-time fallback to the legacy global key. Don't migrate-and-delete —
  // a second Mastodon account on the same device would lose its first-read
  // fallback otherwise. The legacy key ages out naturally as accounts
  // overwrite their per-account slot.
  return appStorage.getString(uiKeys.homeMastodonListLegacy) ?? 'all'
}

export function setHomeMastodonList(accountId: string, listId: string): void {
  appStorage.set(uiKeys.homeMastodonList(accountId), listId)
}

/**
 * Compose the user's stored ordering with the server-side list catalogue:
 * stored ids that no longer exist on the server drop out, server-side lists
 * not yet in the stored order append at the bottom (so a newly-created list
 * is visible without a manual reorder).
 */
export function applyListOrder<T extends { id: string }>(
  serverLists: T[],
  storedOrder: string[]
): T[] {
  if (storedOrder.length === 0) return serverLists
  const byId = new Map(serverLists.map((l) => [l.id, l]))
  const ordered: T[] = []
  const seen = new Set<string>()
  for (const id of storedOrder) {
    const list = byId.get(id)
    if (list && !seen.has(id)) {
      ordered.push(list)
      seen.add(id)
    }
  }
  for (const list of serverLists) {
    if (!seen.has(list.id)) ordered.push(list)
  }
  return ordered
}
