import { create } from 'zustand';
import type { User, InstanceInfoResponse, ReplicatedInstance, AuthResponse, FederationRegistryEntry } from '@backspace/shared';
import { BackspaceApiClient, createApiClient, api } from '../api/client';
import { useAuthStore } from './authStore';
import {
  setApiForOriginResolver,
  setUserIdForOriginResolver,
  setOriginFromHostnameResolver,
} from '../utils/crossStoreResolvers';
import { useSpaceStore } from './spaceStore';
import { connectInstance, disconnectInstance as disconnectWs, disconnectAllRemote } from '../hooks/useWebSocket';
// Circular dependency: federationOps imports useInstanceStore, instanceStore imports this.
// Safe because both modules access each other lazily (at call time, not import time).
// clearPasswordSyncTimers itself does not reference useInstanceStore.
import { clearPasswordSyncTimers } from '../utils/federationOps';
// dmOriginFailover lazily reads useInstanceStore/useSpaceStore/useChatStore at call time,
// so a static import here does not create an import-time cycle.
import { failoverDmOriginsFromDisconnected } from '../utils/dmOriginFailover';
import { useUIStore } from './uiStore';

// ─── Types ───────────────────────────────────────────────────────────────────

export interface ConnectedInstance {
  origin: string;
  label: string;
  token: string;
  user: User;
  username: string;
  status: 'connected' | 'connecting' | 'disconnected' | 'error';
  error?: string;
  api: BackspaceApiClient;
}

interface CachedInstanceToken {
  token: string;
  label: string;
  username: string;
  pendingPasswordSync?: boolean;
}

const STORAGE_KEY_PREFIX = 'backspace_instances';
const LEGACY_STORAGE_KEY = 'backspace_instances';

function storageKey(userId: string): string {
  return `${STORAGE_KEY_PREFIX}_${userId}`;
}

// ─── localStorage helpers ────────────────────────────────────────────────────

function loadCachedTokens(userId: string): Record<string, CachedInstanceToken> {
  try {
    const scopedKey = storageKey(userId);
    const raw = localStorage.getItem(scopedKey);
    if (raw) {
      return JSON.parse(raw) as Record<string, CachedInstanceToken>;
    }

    // One-time migration: adopt legacy unscoped key if it exists
    const legacy = localStorage.getItem(LEGACY_STORAGE_KEY);
    if (legacy) {
      const parsed = JSON.parse(legacy) as Record<string, CachedInstanceToken>;
      localStorage.setItem(scopedKey, legacy);
      localStorage.removeItem(LEGACY_STORAGE_KEY);
      return parsed;
    }

    return {};
  } catch {
    return {};
  }
}

function saveCachedTokens(instances: ConnectedInstance[], userId: string, pendingSyncFlags?: Record<string, boolean>): void {
  const cache: Record<string, CachedInstanceToken> = {};
  // Load existing cache to preserve pendingPasswordSync flags
  const existing = loadCachedTokens(userId);
  for (const inst of instances) {
    // Skip tokenless placeholders — writing an empty token would cause
    // autoConnectAll to find a truthy cached entry with an empty bearer token
    if (!inst.token) continue;
    cache[inst.origin] = {
      token: inst.token,
      label: inst.label,
      username: inst.username,
      pendingPasswordSync: pendingSyncFlags?.[inst.origin] ?? existing[inst.origin]?.pendingPasswordSync,
    };
  }
  localStorage.setItem(storageKey(userId), JSON.stringify(cache));
}

// ─── Network error detection ────────────────────────────────────────────────

/** Detect network-level failures (unreachable, DNS, timeout) vs application errors (401, etc.) */
function isNetworkError(err: unknown): boolean {
  return err instanceof TypeError ||
    (err instanceof Error && /fetch|network|ECONNREFUSED|ETIMEDOUT/i.test(err.message));
}

// ─── Error types ────────────────────────────────────────────────────────────

/** Thrown when the remote instance already has an account for this user with a different password. */
export class DifferentPasswordError extends Error {
  constructor(public remoteUsername: string) {
    super('Account exists with a different password on this instance');
    this.name = 'DifferentPasswordError';
  }
}

// ─── URL normalization ───────────────────────────────────────────────────────

function normalizeOrigin(url: string): string {
  let normalized = url.trim();

  // Add https:// if no protocol
  if (!/^https?:\/\//i.test(normalized)) {
    normalized = `https://${normalized}`;
  }

  try {
    const parsed = new URL(normalized);
    return parsed.origin; // "https://domain.com" — no path, no trailing slash
  } catch {
    throw new Error('Invalid URL');
  }
}

/** Check whether an origin string refers to the current (home) instance. */
export function isSelfOrigin(origin: string): boolean {
  try {
    return normalizeOrigin(origin) === window.location.origin;
  } catch {
    return false;
  }
}

// ─── API client resolution ───────────────────────────────────────────────────

// ─── Registry helpers ────────────────────────────────────────────────────────

function upsertRegistryEntry(
  registry: Map<string, FederationRegistryEntry>,
  origin: string,
  updates: Partial<FederationRegistryEntry> & { origin: string },
): Map<string, FederationRegistryEntry> {
  const next = new Map(registry);
  const existing = next.get(origin);
  if (existing) {
    next.set(origin, { ...existing, ...updates });
  } else {
    next.set(origin, {
      label: '',
      username: '',
      remoteUserId: '',
      status: 'connected',
      addedAt: Date.now(),
      lastConnectedAt: null,
      disconnectedAt: null,
      errorMessage: null,
      ...updates,
    });
  }
  return next;
}

// ─── Store ───────────────────────────────────────────────────────────────────

interface InstanceState {
  instances: ConnectedInstance[];
  isLoading: boolean;
  error: string | null;
  _autoConnectDone: boolean;
  pendingSyncOrigins: string[];
  registry: Map<string, FederationRegistryEntry>;
  registryUpdatedAt: number;
  syncRegistry: () => Promise<void>;
  deleteIdentity: (origins: string[], mode?: 'leave' | 'soft' | 'full') => Promise<Record<string, { success: boolean; error?: string; ownedSpaces?: { id: string; name: string }[] }>>;
  forceRemoveEntry: (origin: string) => void;

  probeInstance: (url: string) => Promise<InstanceInfoResponse & { origin: string }>;
  connectToRemote: (origin: string, password: string, displayName?: string) => Promise<void>;
  loginToRemote: (origin: string, username: string, password: string) => Promise<void>;
  disconnectInstance: (origin: string) => void;
  setInstanceStatus: (origin: string, status: ConnectedInstance['status'], error?: string) => void;
  reconnectInstance: (origin: string) => Promise<void>;
  reauthenticateInstance: (origin: string, password: string) => Promise<void>;
  updateInstanceToken: (origin: string, newToken: string) => void;
  setPendingPasswordSync: (origin: string, pending: boolean) => void;
  hasPendingPasswordSync: (origin: string) => boolean;
  syncInstanceList: () => Promise<void>;
  autoConnectAll: () => Promise<void>;
  reset: () => void;
}

export const useInstanceStore = create<InstanceState>((set, get) => ({
  instances: [],
  isLoading: false,
  error: null,
  _autoConnectDone: false,
  pendingSyncOrigins: [],
  registry: new Map(),
  registryUpdatedAt: 0,

  probeInstance: async (url: string) => {
    const origin = normalizeOrigin(url);

    // Reject self-connection
    if (isSelfOrigin(url)) {
      throw new Error("You're already logged into this instance");
    }

    // Reject duplicates — but only if already connected/connecting.
    // Allow re-adding instances that are in error/disconnected state.
    const existing = get().instances.find(i => i.origin === origin);
    if (existing && (existing.status === 'connected' || existing.status === 'connecting')) {
      throw new Error('This instance is already connected');
    }

    // Probe with unauthenticated client
    const tempClient = createApiClient(origin, () => null);
    const info = await tempClient.instance.info();

    return { ...info, origin };
  },

  connectToRemote: async (origin: string, password: string, displayName?: string) => {
    const currentUser = useAuthStore.getState().user;
    if (!currentUser) throw new Error('Not logged in');

    set({ isLoading: true, error: null });

    try {
      // Step 1: Verify password against the instance we're currently browsing
      const { valid } = await api.users.verifyPassword(password);
      if (!valid) {
        throw new Error('Incorrect password');
      }

      // Step 2: Compute the user's true home identity
      // If we're a federated user (e.g. youruser@nova browsing orbit),
      // homeInstance points to the real home, not window.location.host.
      const trueHomeHost = currentUser.homeInstance ?? window.location.host;
      const bareUsername = currentUser.username.includes('@')
        ? currentUser.username.split('@')[0]!
        : currentUser.username;
      const trueHomeUserId = currentUser.homeUserId ?? currentUser.id;
      const targetHost = new URL(origin).host;
      const targetIsHome = targetHost === trueHomeHost;

      const tempClient = createApiClient(origin, () => null);

      let response: AuthResponse | null = null;
      let finalUsername: string;

      if (targetIsHome) {
        // Target IS the user's home instance — they already have a native account.
        // Just login with bare username, no registration or homeInstance params.
        finalUsername = bareUsername;
        try {
          response = await tempClient.auth.login({
            username: bareUsername,
            password,
          });
        } catch {
          throw new DifferentPasswordError(bareUsername);
        }
      } else {
        // Target is a remote/third-party instance — register as user@homeHost
        finalUsername = `${bareUsername}@${trueHomeHost}`;

        // 2a: Attempt registration with namespaced username
        try {
          response = await tempClient.auth.register({
            username: finalUsername,
            password,
            displayName: displayName || currentUser.displayName || undefined,
            homeInstance: trueHomeHost,
            homeUserId: trueHomeUserId,
          });
        } catch (err) {
          const message = (err as Error).message;
          if (message.includes('already taken') || message.includes('409') ||
              message.includes('Registration is currently closed') || message.includes('403')) {
            // Already registered or registration closed — fall through to login
          } else {
            throw err;
          }
        }

        // 2b: If registration didn't work, try login
        if (!response) {
          try {
            response = await tempClient.auth.login({
              username: finalUsername,
              password,
            });
          } catch {
            // Namespaced login failed — try legacy plain username as fallback
            try {
              response = await tempClient.auth.login({
                username: bareUsername,
                password,
              });
            } catch {
              throw new DifferentPasswordError(bareUsername);
            }
          }
        }
      }

      if (!response) {
        throw new Error('Failed to authenticate with remote instance');
      }

      // Step 3: Complete connection
      const info = await tempClient.instance.info();
      const authenticatedClient = createApiClient(origin, () => response.token);

      const instance: ConnectedInstance = {
        origin,
        label: info.name,
        token: response.token,
        user: response.user,
        username: finalUsername,
        status: 'connected',
        api: authenticatedClient,
      };

      set((state) => {
        const updated = [...state.instances, instance];
        saveCachedTokens(updated, currentUser.id);
        return { instances: updated, isLoading: false };
      });

      // Upsert registry entry for the new connection
      const registry = upsertRegistryEntry(get().registry, origin, {
        origin,
        label: instance.label,
        username: instance.username,
        remoteUserId: instance.user.id,
        status: 'connected',
        addedAt: get().registry.get(origin)?.addedAt ?? Date.now(),
        lastConnectedAt: Date.now(),
        disconnectedAt: null,
        errorMessage: null,
      });
      const registryUpdatedAt = Date.now();
      set({ registry, registryUpdatedAt });

      // Open WebSocket connection to the remote instance
      connectInstance(origin, response.token);

      // Ensure server-to-server peering for DM relay (non-fatal)
      try {
        const peerResult = await api.federation.ensurePeered({ remoteOrigin: origin });
        if (peerResult.peeringStatus === 'rejected') {
          const { addToast } = useUIStore.getState();
          addToast(
            `Cross-instance messaging unavailable — ${instance.label} requires manual peering approval`,
            'warning',
            10000,
          );
        } else if (peerResult.peeringStatus === 'pending') {
          const { addToast } = useUIStore.getState();
          addToast(
            `Peering with ${instance.label} in progress — cross-instance messaging will be available shortly`,
            'info',
          );
        }
      } catch (err) {
        console.warn('[federation] Peering attempt failed (non-fatal):', err);
      }

      // Sync instance list to all instances (fire-and-forget)
      get().syncInstanceList().catch(() => {});
      get().syncRegistry().catch(() => {});
    } catch (err) {
      set({ isLoading: false, error: (err as Error).message });
      throw err;
    }
  },

  loginToRemote: async (origin: string, username: string, password: string) => {
    set({ isLoading: true, error: null });

    try {
      const tempClient = createApiClient(origin, () => null);
      const response = await tempClient.auth.login({ username, password });

      // Fetch instance info for the label
      const info = await tempClient.instance.info();

      const authenticatedClient = createApiClient(origin, () => response.token);

      const instance: ConnectedInstance = {
        origin,
        label: info.name,
        token: response.token,
        user: response.user,
        username: response.user.username,
        status: 'connected',
        api: authenticatedClient,
      };

      set((state) => {
        const updated = [...state.instances, instance];
        const userId = useAuthStore.getState().user?.id;
        if (userId) saveCachedTokens(updated, userId);
        return { instances: updated, isLoading: false };
      });

      // Upsert registry entry for the login connection
      const registry = upsertRegistryEntry(get().registry, origin, {
        origin,
        label: instance.label,
        username: instance.username,
        remoteUserId: instance.user.id,
        status: 'connected',
        addedAt: get().registry.get(origin)?.addedAt ?? Date.now(),
        lastConnectedAt: Date.now(),
        disconnectedAt: null,
        errorMessage: null,
      });
      const registryUpdatedAt = Date.now();
      set({ registry, registryUpdatedAt });

      // Open WebSocket connection to the remote instance
      connectInstance(origin, response.token);

      // Sync instance list to all instances (fire-and-forget)
      get().syncInstanceList().catch(() => {});
      get().syncRegistry().catch(() => {});
    } catch (err) {
      set({ isLoading: false, error: (err as Error).message });
      throw err;
    }
  },

  setInstanceStatus: (origin, status, error) => {
    const prev = get().instances.find(i => i.origin === origin)?.status;
    set((state) => ({
      instances: state.instances.map(i =>
        i.origin === origin ? { ...i, status, error } : i
      ),
    }));
    if (prev === 'connected' && (status === 'disconnected' || status === 'error')) {
      failoverDmOriginsFromDisconnected(origin);
    }
  },

  disconnectInstance: (origin: string) => {
    // Tear down WebSocket connection (stops auto-reconnect)
    disconnectWs(origin);

    // Update registry entry to disconnected
    const registry = upsertRegistryEntry(get().registry, origin, {
      origin,
      status: 'disconnected',
      disconnectedAt: Date.now(),
      errorMessage: null,
    });
    const registryUpdatedAt = Date.now();

    // Keep the instance in the array with status 'disconnected' — preserves
    // the token and API client so reconnect is instant (no re-auth needed).
    set((state) => {
      const updated = state.instances.map(i =>
        i.origin === origin ? { ...i, status: 'disconnected' as const, error: undefined } : i
      );
      const userId = useAuthStore.getState().user?.id;
      if (userId) saveCachedTokens(updated, userId);
      return { instances: updated, registry, registryUpdatedAt };
    });

    // Failover DMs to a connected sibling BEFORE removeInstanceSpaces wipes
    // this origin's pins. DMs with a connected alternative survive via rekey;
    // DMs without one are removed alongside the rest of the instance's content.
    failoverDmOriginsFromDisconnected(origin);
    useSpaceStore.getState().removeInstanceSpaces(origin);

    // Sync updated lists to remaining instances (fire-and-forget)
    get().syncInstanceList().catch(() => {});
    get().syncRegistry().catch(() => {});
  },

  reconnectInstance: async (origin: string) => {
    let inst = get().instances.find(i => i.origin === origin);

    // If the instance was disconnected (removed from active instances array) but
    // has a cached token in localStorage, restore it so reconnect can proceed.
    if (!inst) {
      const userId = useAuthStore.getState().user?.id;
      if (!userId) return;
      const cached = loadCachedTokens(userId);
      const entry = cached[origin];
      if (!entry?.token) return; // No cached token — needs full re-authentication via connectToRemote

      const currentUser = useAuthStore.getState().user;
      if (!currentUser) return;

      const client = createApiClient(origin, () => entry.token);
      const restoredInstance: ConnectedInstance = {
        origin,
        label: entry.label || new URL(origin).host,
        token: entry.token,
        user: currentUser,
        username: entry.username || '',
        status: 'connecting' as const,
        api: client,
      };

      set((state) => ({
        instances: [...state.instances, restoredInstance],
      }));

      inst = restoredInstance;
    }

    if (inst.status === 'connected' || inst.status === 'connecting') return;

    // Tokenless placeholders can't reconnect — they need full re-authentication
    if (!inst.token) return;

    // Set to connecting
    set((state) => ({
      instances: state.instances.map(i =>
        i.origin === origin ? { ...i, status: 'connecting' as const, error: undefined } : i
      ),
    }));

    try {
      const user = await inst.api.users.me();

      set((state) => ({
        instances: state.instances.map(i =>
          i.origin === origin ? { ...i, status: 'connected' as const, user, error: undefined } : i
        ),
      }));

      // Update registry entry on successful reconnect
      const registry = upsertRegistryEntry(get().registry, origin, {
        origin,
        status: 'connected',
        lastConnectedAt: Date.now(),
        disconnectedAt: null,
        errorMessage: null,
      });
      const registryUpdatedAt = Date.now();
      set({ registry, registryUpdatedAt });
      get().syncRegistry().catch(() => {});

      connectInstance(origin, inst.token);
    } catch (err) {
      if (isNetworkError(err)) {
        set((state) => ({
          instances: state.instances.map(i =>
            i.origin === origin
              ? { ...i, status: 'disconnected' as const, error: 'Instance unreachable — retrying in background' }
              : i
          ),
        }));

        // Update registry on network error
        const errRegistry = upsertRegistryEntry(get().registry, origin, {
          origin,
          status: 'unreachable',
          errorMessage: 'Instance unreachable',
        });
        set({ registry: errRegistry, registryUpdatedAt: Date.now() });

        connectInstance(origin, inst.token);
      } else {
        set((state) => ({
          instances: state.instances.map(i =>
            i.origin === origin
              ? { ...i, status: 'error' as const, error: 'Token expired — re-authenticate to reconnect' }
              : i
          ),
        }));

        // Update registry on auth error
        const errRegistry = upsertRegistryEntry(get().registry, origin, {
          origin,
          status: 'auth_expired',
          errorMessage: 'Token expired',
        });
        set({ registry: errRegistry, registryUpdatedAt: Date.now() });
      }
    }
  },

  reauthenticateInstance: async (origin: string, password: string) => {
    const inst = get().instances.find(i => i.origin === origin);

    // Clean up existing instance if present (stale placeholder or disconnected entry)
    if (inst) {
      set((state) => ({
        instances: state.instances.filter(i => i.origin !== origin),
      }));
      useSpaceStore.getState().removeInstanceSpaces(origin);
    }

    // Disconnect any lingering WS
    disconnectWs(origin);

    // Re-connect through the standard flow (handles register/login)
    const currentUser = useAuthStore.getState().user;
    if (!currentUser) return;

    await get().connectToRemote(
      origin,
      password,
      currentUser.displayName || undefined,
    );

    // Clear pending password sync — connectToRemote uses the current password
    // which updates the remote's stored hash through register/login
    get().setPendingPasswordSync(origin, false);
  },

  updateInstanceToken: (origin: string, newToken: string) => {
    set((state) => ({
      instances: state.instances.map(i => {
        if (i.origin !== origin) return i;
        // Recreate API client with new token
        const newApi = createApiClient(origin, () => newToken);
        return { ...i, token: newToken, api: newApi };
      }),
    }));

    const userId = useAuthStore.getState().user?.id;
    if (userId) saveCachedTokens(get().instances, userId);

    // Reconnect WebSocket with new token
    disconnectWs(origin);
    connectInstance(origin, newToken);
  },

  setPendingPasswordSync: (origin: string, pending: boolean) => {
    const userId = useAuthStore.getState().user?.id;
    if (!userId) return;

    // Update Zustand state (triggers React re-renders)
    set((state) => ({
      pendingSyncOrigins: pending
        ? state.pendingSyncOrigins.includes(origin)
          ? state.pendingSyncOrigins
          : [...state.pendingSyncOrigins, origin]
        : state.pendingSyncOrigins.filter(o => o !== origin),
    }));

    // Also persist to localStorage
    const flags: Record<string, boolean> = { [origin]: pending };
    saveCachedTokens(get().instances, userId, flags);
  },

  hasPendingPasswordSync: (origin: string) => {
    return get().pendingSyncOrigins.includes(origin);
  },

  syncInstanceList: async () => {
    // Prevent premature sync before autoConnectAll has populated all server-known
    // instances — otherwise a user action (add/remove) would overwrite the server
    // record with only the currently-loaded subset, permanently erasing the rest
    if (!get()._autoConnectDone) return;

    const { instances } = get();
    const currentUser = useAuthStore.getState().user;
    if (!currentUser) return;

    // Build perspective-correct replicated instance lists.
    // Each instance should store references to OTHER instances, never itself.
    const homeOrigin = window.location.origin;
    const homeUsername = currentUser.username.includes('@')
      ? currentUser.username.split('@')[0]!
      : currentUser.username;

    // Home list: all remotes (home never references itself)
    const homeList: ReplicatedInstance[] = instances.map(inst => ({
      origin: inst.origin,
      username: inst.username,
    }));

    // Push to home instance
    const homePromise = api.users.update({ replicatedInstances: homeList }).catch((err) => {
      console.warn('Failed to sync instance list to home:', err);
    });

    // Push perspective-correct list to each remote instance:
    // include home + all OTHER remotes, but exclude the remote's own origin
    const connectedInstances = instances.filter(inst => inst.status === 'connected');
    const remotePromises = connectedInstances.map(inst => {
      const listForRemote: ReplicatedInstance[] = [
        { origin: homeOrigin, username: homeUsername },
        ...instances
          .filter(other => other.origin !== inst.origin)
          .map(other => ({ origin: other.origin, username: other.username })),
      ];
      return inst.api.users.update({ replicatedInstances: listForRemote }).catch((err) => {
        console.warn(`Failed to sync instance list to ${inst.origin}:`, err);
      });
    });

    await Promise.all([homePromise, ...remotePromises]);
  },

  syncRegistry: async () => {
    if (!get()._autoConnectDone) return;

    const { registry, registryUpdatedAt, instances } = get();
    const currentUser = useAuthStore.getState().user;
    if (!currentUser) return;

    const entries = Array.from(registry.values());
    const payload = { registry: entries, updatedAt: registryUpdatedAt };

    // Push to home instance
    const homePromise = api.users.putFederationRegistry(payload).catch((err) => {
      console.warn('Failed to sync registry to home:', err);
    });

    // Push to all connected remote instances
    const connectedInstances = instances.filter(i => i.status === 'connected');
    const remotePromises = connectedInstances.map(inst =>
      inst.api.users.putFederationRegistry(payload).catch((err) => {
        console.warn(`Failed to sync registry to ${inst.origin}:`, err);
      })
    );

    await Promise.all([homePromise, ...remotePromises]);
  },

  deleteIdentity: async (origins: string[], mode: 'leave' | 'soft' | 'full' = 'leave') => {
    try {
      const { results } = await api.users.deleteFederationIdentity({ origins, mode });

      // Clean up client-side state for successful deletions
      for (const [origin, result] of Object.entries(results)) {
        if (result.success) {
          get().forceRemoveEntry(origin);
        }
      }

      return results;
    } catch (err) {
      const error = err instanceof Error ? err.message : 'Unknown error';
      return Object.fromEntries(origins.map(o => [o, { success: false as const, error }]));
    }
  },

  forceRemoveEntry: (origin: string) => {
    // Tear down WebSocket if connected
    disconnectWs(origin);

    // Remove from registry
    const registry = new Map(get().registry);
    registry.delete(origin);
    const registryUpdatedAt = Date.now();

    // Remove from instances and purge token from localStorage
    set((state) => {
      const updated = state.instances.filter(i => i.origin !== origin);
      const userId = useAuthStore.getState().user?.id;
      if (userId) saveCachedTokens(updated, userId);
      return { instances: updated, registry, registryUpdatedAt };
    });

    // Same rationale as disconnectInstance — preserve DMs with connected alts.
    failoverDmOriginsFromDisconnected(origin);
    useSpaceStore.getState().removeInstanceSpaces(origin);

    get().syncRegistry().catch(() => {});
  },

  autoConnectAll: async () => {
    const currentUser = useAuthStore.getState().user;
    if (!currentUser) {
      set({ _autoConnectDone: true });
      return;
    }

    const cached = loadCachedTokens(currentUser.id);

    // Fetch server-side registry (source of truth for entry list)
    let serverRegistry: FederationRegistryEntry[] = [];
    let serverRegistryUpdatedAt = 0;
    try {
      const res = await api.users.getFederationRegistry();
      serverRegistry = res.registry;
      serverRegistryUpdatedAt = res.updatedAt;
    } catch (err) {
      console.warn('Failed to fetch federation registry from home:', err);
    }

    // Initialize registry from server data
    const registry = new Map<string, FederationRegistryEntry>();
    for (const entry of serverRegistry) {
      registry.set(entry.origin, entry);
    }

    // Migration: promote localStorage-only entries to registry
    for (const [origin] of Object.entries(cached)) {
      if (origin === window.location.origin) continue;
      if (!registry.has(origin)) {
        registry.set(origin, {
          origin,
          label: cached[origin]?.label || new URL(origin).host,
          username: cached[origin]?.username || '',
          remoteUserId: '',
          status: 'connected',
          addedAt: Date.now(),
          lastConnectedAt: Date.now(),
          disconnectedAt: null,
          errorMessage: null,
        });
      }
    }

    // If logged in as a federated user, include the home instance as a
    // connection target. It won't be in replicatedInstances (you don't
    // "federate to" your own home), but the client needs it for friends,
    // DMs, and profile data.
    const instancesToConnect = [...currentUser.replicatedInstances];
    if (currentUser.homeInstance) {
      const homeOrigin = `https://${currentUser.homeInstance}`;
      if (!isSelfOrigin(homeOrigin)) {
        // Compute bare username (strip @domain suffix if present)
        const bareUsername = currentUser.username.includes('@')
          ? currentUser.username.split('@')[0]!
          : currentUser.username;

        const alreadyIncluded = instancesToConnect.some(ri =>
          (ri.origin || `https://${ri.domain}`) === homeOrigin
        );
        if (!alreadyIncluded) {
          instancesToConnect.push({
            origin: homeOrigin,
            username: bareUsername,
            domain: currentUser.homeInstance,
          });
        }

        // Ensure the home instance has a registry entry so it appears
        // in the Connections UI (the registry is the source of truth for
        // the Connections panel, not the instances array).
        if (!registry.has(homeOrigin)) {
          registry.set(homeOrigin, {
            origin: homeOrigin,
            label: currentUser.homeInstance,
            username: bareUsername,
            remoteUserId: currentUser.homeUserId ?? '',
            status: 'auth_expired',
            addedAt: Date.now(),
            lastConnectedAt: null,
            disconnectedAt: null,
            errorMessage: 'Authenticate to connect to your home instance',
          });
        }
      }
    }

    // Early return if there's nothing to connect
    if (instancesToConnect.length === 0 && registry.size === 0) {
      set({ _autoConnectDone: true });
      return;
    }

    // Split server-known instances into three groups:
    // - withToken: have a cached token and should auto-connect
    // - withoutToken: no cached token → add as error placeholder
    // - userDisconnected: user explicitly disconnected → add as disconnected placeholder (no auto-connect)
    const withToken: Array<{ origin: string; ri: (typeof instancesToConnect)[0]; entry: CachedInstanceToken }> = [];
    const withoutToken: Array<{ origin: string; ri: (typeof instancesToConnect)[0] }> = [];
    const userDisconnected: Array<{ origin: string; ri: (typeof instancesToConnect)[0]; entry: CachedInstanceToken }> = [];

    for (const ri of instancesToConnect) {
      const origin = ri.origin || `https://${ri.domain}`;
      // Never connect to ourselves — home WS is managed separately
      if (isSelfOrigin(origin)) continue;
      if (get().instances.some(i => i.origin === origin)) continue; // already loaded
      const cachedEntry = cached[origin];
      const regEntry = registry.get(origin);
      if (cachedEntry) {
        // Respect user's explicit disconnect — don't auto-reconnect
        if (regEntry?.status === 'disconnected') {
          userDisconnected.push({ origin, ri, entry: cachedEntry });
        } else {
          withToken.push({ origin, ri, entry: cachedEntry });
        }
      } else {
        withoutToken.push({ origin, ri });
      }
    }

    // Add user-disconnected instances as disconnected placeholders (token preserved
    // so reconnect is instant, but no WebSocket or API calls until user clicks reconnect)
    if (userDisconnected.length > 0) {
      set((state) => {
        const placeholders: ConnectedInstance[] = userDisconnected.map(({ origin, entry: cachedEntry }) => ({
          origin,
          label: cachedEntry.label || new URL(origin).host,
          token: cachedEntry.token,
          user: currentUser,
          username: cachedEntry.username || '',
          status: 'disconnected' as const,
          api: createApiClient(origin, () => cachedEntry.token),
        }));
        return { instances: [...state.instances, ...placeholders] };
      });
    }

    // Immediately add tokenless placeholders so they're visible in Zustand
    // (and therefore won't be erased by syncInstanceList)
    if (withoutToken.length > 0) {
      set((state) => {
        const placeholders: ConnectedInstance[] = withoutToken.map(({ origin, ri }) => ({
          origin,
          label: new URL(origin).host,
          token: '',
          user: currentUser, // placeholder
          username: ri.username,
          status: 'error' as const,
          error: 'Session expired — re-authenticate to reconnect',
          api: createApiClient(origin, () => null),
        }));
        return { instances: [...state.instances, ...placeholders] };
      });
    }

    // Update registry for tokenless placeholders
    for (const { origin } of withoutToken) {
      const entry = registry.get(origin);
      if (entry) {
        registry.set(origin, { ...entry, status: 'auth_expired', errorMessage: 'Session expired — re-authenticate to reconnect' });
      }
    }

    // Connect instances with cached tokens in parallel
    if (withToken.length > 0) {
      const results = await Promise.allSettled(
        withToken.map(async ({ origin, ri, entry: cachedEntry }) => {
          // Create client with cached token
          const client = createApiClient(origin, () => cachedEntry.token);

          // Set as connecting
          const connectingInstance: ConnectedInstance = {
            origin,
            label: cachedEntry.label || new URL(origin).host,
            token: cachedEntry.token,
            user: currentUser, // Placeholder until we verify
            username: cachedEntry.username || ri.username,
            status: 'connecting',
            api: client,
          };

          set((state) => ({
            instances: [...state.instances.filter(i => i.origin !== origin), connectingInstance],
          }));

          try {
            // Verify the token is still valid
            const user = await client.users.me();

            // Fetch instance info for fresh label
            let label = cachedEntry.label || new URL(origin).host;
            try {
              const info = await client.instance.info();
              label = info.name;
            } catch {
              // Non-critical — keep cached label
            }

            // Backfill homeUserId if missing (existing federated users before this field existed)
            if (user.homeInstance && !user.homeUserId) {
              const homeUser = useAuthStore.getState().user;
              if (homeUser) {
                client.users.update({ homeUserId: homeUser.id }).catch(() => {});
              }
            }

            // Backfill cached username if stale after server-side migration
            // (e.g. "test" was renamed to "test@nova.ddns.net")
            if (user.username !== cachedEntry.username) {
              cachedEntry.username = user.username;
            }

            const connectedInstance: ConnectedInstance = {
              origin,
              label,
              token: cachedEntry.token,
              user,
              username: user.username,
              status: 'connected',
              api: client,
            };

            set((state) => ({
              instances: state.instances.map(i => i.origin === origin ? connectedInstance : i),
            }));

            // Update registry entry on successful reconnect
            const entry = registry.get(origin);
            if (entry) {
              registry.set(origin, { ...entry, status: 'connected', lastConnectedAt: Date.now(), disconnectedAt: null, errorMessage: null, remoteUserId: user.id, label });
            }

            // Open WebSocket connection now that we've verified the token
            connectInstance(origin, cachedEntry.token);

            // Initiate server-to-server peering for DM relay (non-fatal, idempotent)
            api.federation.ensurePeered({ remoteOrigin: origin }).catch(() => {});
          } catch (err) {
            if (isNetworkError(err)) {
              // Instance unreachable (NAT hairpinning, DNS, server down) — token may still be valid
              set((state) => ({
                instances: state.instances.map(i =>
                  i.origin === origin
                    ? { ...i, status: 'disconnected' as const, error: 'Instance unreachable — retrying in background' }
                    : i
                ),
              }));

              // Update registry entry on network error
              const entry = registry.get(origin);
              if (entry) {
                registry.set(origin, { ...entry, status: 'unreachable', errorMessage: 'Instance unreachable' });
              }

              // Start WebSocket — its built-in exponential backoff retry will auto-recover
              // when the network path becomes available (e.g. user switches networks)
              connectInstance(origin, cachedEntry.token);
            } else {
              // Auth failure (401, invalid token, etc.)
              set((state) => ({
                instances: state.instances.map(i =>
                  i.origin === origin
                    ? { ...i, status: 'error' as const, error: 'Token expired — re-authenticate to reconnect' }
                    : i
                ),
              }));

              // Update registry entry on auth error
              const entry = registry.get(origin);
              if (entry) {
                registry.set(origin, { ...entry, status: 'auth_expired', errorMessage: 'Token expired' });
              }
            }
          }
        })
      );

      // Log any failures for debugging
      const failures = results.filter(r => r.status === 'rejected');
      if (failures.length > 0) {
        console.warn(`autoConnectAll: ${failures.length}/${withToken.length} instances failed to connect`);
      }
    }

    // Save final state to localStorage — persist ALL instances regardless of status
    // so tokens are preserved for instant reconnect (registry controls auto-connect behavior)
    saveCachedTokens(get().instances, currentUser.id);

    // Hydrate pendingSyncOrigins from localStorage cache and mark auto-connect done
    const freshCached = loadCachedTokens(currentUser.id);
    const pendingOrigins = Object.entries(freshCached)
      .filter(([, v]) => v.pendingPasswordSync)
      .map(([origin]) => origin);

    // Persist reconciled registry
    const registryUpdatedAt = serverRegistryUpdatedAt > 0 ? Math.max(serverRegistryUpdatedAt, Date.now()) : Date.now();
    set({ _autoConnectDone: true, pendingSyncOrigins: pendingOrigins, registry, registryUpdatedAt });

    // Sync reconciled registry to all instances
    get().syncRegistry().catch(() => {});
  },

  reset: () => {
    // Clean up space store for each connected remote instance before tearing down
    const { instances } = get();
    for (const inst of instances) {
      useSpaceStore.getState().removeInstanceSpaces(inst.origin);
    }

    // Tear down all remote WebSocket connections
    disconnectAllRemote();

    clearPasswordSyncTimers();

    set({ instances: [], isLoading: false, error: null, _autoConnectDone: false, pendingSyncOrigins: [], registry: new Map(), registryUpdatedAt: 0 });
    // Token cache preserved — scoped per user, survives logout for seamless reconnect
  },
}));

// ─── API client resolution ───────────────────────────────────────────────────
// Register the resolver with spaceStore so getApiForOrigin() works everywhere.
// Placed after store creation so useInstanceStore is definitely initialized.
// This breaks the circular dependency: chatStore → spaceStore ← instanceStore
// instead of: chatStore → instanceStore → useWebSocket → chatStore (cycle).

setApiForOriginResolver((origin: string): BackspaceApiClient => {
  if (!origin) return api;
  const instance = useInstanceStore.getState().instances.find(i => i.origin === origin);
  if (!instance) return api;
  return instance.api;
});

// ─── User ID resolution (federation) ──────────────────────────────────────────
// Maps an origin to the local user's ID on that remote instance.
// Used by voice join/leave to optimistically add/remove the correct user ID.

setUserIdForOriginResolver((origin: string): string | undefined => {
  const instance = useInstanceStore.getState().instances.find(i => i.origin === origin);
  return instance?.user.id;
});

// ─── Hostname → origin resolution (federation) ────────────────────────────────
// Maps a user's homeInstance hostname to its full origin URL.

setOriginFromHostnameResolver((hostname: string): string => {
  const inst = useInstanceStore.getState().instances.find(i => {
    try { return new URL(i.origin).host === hostname; } catch { return false; }
  });
  return inst?.origin ?? '';
});
