import { computed, ref, toRaw, watch } from 'vue';
import { defineStore } from 'pinia';
import {
    MESSAGE_MAILBOX_NATIONAL_BASE,
    MESSAGE_MAILBOX_PUBLIC,
    type MessageType,
} from '@sammo-ts/logic/messages/message.js';
import { applyReadModelDelta, cloneReadModelJson } from '@sammo-ts/common/realtime/delta';
import type { PublicRealtimeEvent, RealtimeReadModelInvalidation } from '@sammo-ts/common/realtime/types';
import { trpc } from '../utils/trpc';
import { useMapViewerStore } from './mapViewer';
import { useSessionStore } from './session';
import { createLatestRefreshQueue } from '../utils/latestRefreshQueue';
import { createRateLimitedRefreshQueue } from '../utils/rateLimitedRefreshQueue';
import { structurallyShare } from '../utils/structuralShare';
import {
    createMergedReadModelRefreshQueue,
    resolveDashboardContextBundleInclude,
    type DashboardContextBundleInclude,
} from '../utils/dashboardReadModel';
import { createBroadcastTabCoordinator, type BroadcastTabCoordinator } from '../utils/broadcastTabCoordinator';
import { resolveWithReadModelSnapshotFallback } from '../utils/readModelDeltaRecovery';
import { createRealtimeRequestOptions } from '../utils/realtimeAccessGrant';
import { markGameServerContact } from '../utils/gameServerActivity';
import { GAME_SERVER_RECONNECTED_EVENT } from '../utils/gameServerConnection';
import { gameFrontendRuntimeConfig } from '../config/runtimeConfig';

const REALTIME_FULL_REFRESH_MIN_INTERVAL_MS = 5_000;

const resolveErrorMessage = (value: unknown): string => {
    if (value instanceof Error) {
        return value.message;
    }
    if (typeof value === 'string') {
        return value;
    }
    return 'unknown_error';
};

export const useMainDashboardStore = defineStore('mainDashboard', () => {
    type GeneralContext = Awaited<ReturnType<typeof trpc.general.me.query>>;
    type PresentGeneralContext = NonNullable<GeneralContext>;
    type LobbyInfo = Awaited<ReturnType<typeof trpc.lobby.info.query>>;
    type WorldMapResult = Awaited<ReturnType<typeof trpc.world.getMap.query>>;
    type MapLayout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
    type CommandTable = Awaited<ReturnType<typeof trpc.turns.getCommandTable.query>>;
    type MessageBundle = Awaited<ReturnType<typeof trpc.messages.getRecent.query>>;
    type PrivateMessageNotice = { messageId: number };
    type MessageContacts = Awaited<ReturnType<typeof trpc.messages.getContacts.query>>;
    type BoardAccess = Awaited<ReturnType<typeof trpc.board.getAccess.query>>;
    type ReservedTurnView = Awaited<ReturnType<typeof trpc.turns.reserved.getGeneral.query>>['turns'][number];
    type RecentRecord = Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>['global'][number];
    type FrontStatus = Awaited<ReturnType<typeof trpc.general.getFrontStatus.query>>;
    type TournamentState = Awaited<ReturnType<typeof trpc.tournament.getState.query>>;
    type TournamentType = NonNullable<TournamentState>['type'];
    type ContextBundleDelta = Awaited<ReturnType<typeof trpc.dashboard.getContextBundleDelta.query>>;
    type DashboardReadModelPatch = {
        contextSnapshot?: GeneralContext;
        contextRevision?: string | null;
        commandTableRevision?: string | null;
        boardAccessRevision?: string | null;
        contextSourceRevision?: string | null;
        commandTableSourceRevision?: string | null;
        boardAccessSourceRevision?: string | null;
        general?: PresentGeneralContext['general'] | null;
        city?: PresentGeneralContext['city'] | null;
        nation?: PresentGeneralContext['nation'] | null;
        lobbyInfo?: LobbyInfo | null;
        worldMap?: WorldMapResult | null;
        mapLayout?: MapLayout | null;
        commandTable?: CommandTable | null;
        messages?: MessageBundle | null;
        messageContacts?: MessageContacts | null;
        boardAccess?: BoardAccess | null;
        reservedGeneralTurns?: ReservedTurnView[] | null;
        reservedGeneralRevision?: number;
        reservedGeneralAutorunLimit?: number | null;
        globalRecords?: RecentRecord[];
        generalRecords?: RecentRecord[];
        worldHistory?: RecentRecord[];
        frontStatus?: FrontStatus | null;
        tournamentStage?: number;
        tournamentType?: TournamentType | null;
    };
    type DashboardTabMessage =
        | { kind: 'patch'; patch: DashboardReadModelPatch }
        | {
              kind: 'status';
              status: 'idle' | 'connected';
              turnEngineRunning?: boolean | null;
          };

    const loading = ref(false);
    const refreshing = ref(false);
    const error = ref<string | null>(null);
    const recordsError = ref<string | null>(null);
    const frontStatusError = ref<string | null>(null);
    const realtimeEnabled = ref(true);
    const realtimeStatus = ref<'idle' | 'connected' | 'paused'>('idle');
    const realtimeActive = ref(false);
    const accessLimited = ref(false);

    const handleDashboardError = (value: unknown) => {
        const message = resolveErrorMessage(value);
        error.value = message;
        if (message.startsWith('접속 제한중입니다.')) {
            accessLimited.value = true;
            realtimeStatus.value = 'paused';
        }
    };

    const general = ref<PresentGeneralContext['general'] | null>(null);
    const city = ref<PresentGeneralContext['city'] | null>(null);
    const nation = ref<PresentGeneralContext['nation'] | null>(null);
    const lobbyInfo = ref<LobbyInfo | null>(null);
    const worldMap = ref<WorldMapResult | null>(null);
    const mapLayout = ref<MapLayout | null>(null);
    const commandTable = ref<CommandTable | null>(null);
    const messages = ref<MessageBundle | null>(null);
    const messageContacts = ref<MessageContacts | null>(null);
    const boardAccess = ref<BoardAccess | null>(null);
    const reservedGeneralTurns = ref<ReservedTurnView[] | null>(null);
    const reservedGeneralRevision = ref(0);
    const reservedGeneralAutorunLimit = ref<number | null>(null);
    const globalRecords = ref<RecentRecord[]>([]);
    const generalRecords = ref<RecentRecord[]>([]);
    const worldHistory = ref<RecentRecord[]>([]);
    const frontStatus = ref<FrontStatus | null>(null);
    const tournamentStage = ref(0);
    const tournamentType = ref<TournamentType | null>(null);
    const surveyNotice = ref<NonNullable<FrontStatus['latestVote']> | null>(null);
    const privateMessageNotice = ref<PrivateMessageNotice | null>(null);
    let dismissedPrivateMessageId = 0;
    let lastGeneralRecordId = 0;
    let lastWorldHistoryId = 0;
    let recordGeneralId: number | null = null;
    let initialized = false;
    let contextSnapshot: GeneralContext | undefined;
    let commandTableSnapshot: CommandTable | undefined;
    let boardAccessSnapshot: BoardAccess | undefined;
    let contextRevision: string | null = null;
    let commandTableRevision: string | null = null;
    let boardAccessRevision: string | null = null;
    let contextSourceRevision: string | null = null;
    let commandTableSourceRevision: string | null = null;
    let boardAccessSourceRevision: string | null = null;

    const messageDraftText = ref('');
    const targetMailbox = ref<number>(MESSAGE_MAILBOX_PUBLIC);
    let initializedMailboxGeneralId: number | null = null;

    const generalId = computed(() => general.value?.id ?? null);
    const nationId = computed(() => nation.value?.id ?? null);
    const mapViewer = useMapViewerStore();
    const session = useSessionStore();

    const selectedCity = computed(() => {
        const layout = mapLayout.value;
        const map = worldMap.value;
        const selectedId = mapViewer.selectedCityId;
        if (!layout || !map || !selectedId) {
            return null;
        }
        const layoutCity = layout.cityList.find((city) => city.id === selectedId);
        const mapEntry = map.cityList.find((entry) => entry[0] === selectedId);
        if (!layoutCity || !mapEntry) {
            return null;
        }
        const [, level, state, nationIdValue, region, supplyFlag] = mapEntry;
        const nationEntry = map.nationList.find((nationEntry) => nationEntry[0] === nationIdValue);
        const regionName = layout.regionMap[region] ?? '-';
        const levelName = layout.levelMap[level] ?? '-';

        return {
            id: layoutCity.id,
            name: layoutCity.name,
            level,
            levelName,
            region,
            regionName,
            nationId: nationIdValue,
            nationName: nationEntry?.[1] ?? '무주',
            nationColor: nationEntry?.[2] ?? '#444444',
            state,
            supply: supplyFlag > 0,
            isCapital: nationEntry?.[3] === layoutCity.id,
            isMyCity: map.myCity === layoutCity.id,
        } as const;
    });

    const mailboxGroups = computed(() => {
        type MailboxOption = {
            label: string;
            value: number;
            disabled?: boolean;
            color?: string;
        };
        type MailboxGroup = {
            label: string;
            color?: string;
            options: MailboxOption[];
        };

        const ownNationId = nationId.value ?? 0;
        const ownMailbox = MESSAGE_MAILBOX_NATIONAL_BASE + ownNationId;
        const permission = messages.value?.permission ?? -1;
        const contacts = messageContacts.value?.nation ?? [];
        const ownNation = contacts.find((nation) => nation.mailbox === ownMailbox);
        const groups: MailboxGroup[] = [
            {
                label: '즐겨찾기',
                color: '#000000',
                options: [
                    {
                        label: '【 아국 메세지 】',
                        value: ownMailbox,
                        color: ownNation?.color ?? '#000000',
                    },
                    {
                        label: '【 전체 메세지 】',
                        value: MESSAGE_MAILBOX_PUBLIC,
                        color: '#000000',
                    },
                ],
            },
        ];

        if (permission >= 4) {
            groups.push({
                label: '외교메시지',
                color: '#000000',
                options: contacts
                    .filter((nation) => nation.mailbox !== ownMailbox)
                    .map((nation) => ({
                        label: nation.name,
                        value: nation.mailbox,
                        color: nation.color,
                    })),
            });
        }

        const sortedContacts = [...contacts].sort((left, right) => {
            if (left.mailbox === ownMailbox) return -1;
            if (right.mailbox === ownMailbox) return 1;
            return left.mailbox - right.mailbox;
        });
        for (const nation of sortedContacts) {
            const options = [...nation.general]
                .filter(([id]) => id !== generalId.value)
                .sort((left, right) => left[1].localeCompare(right[1], 'ko'))
                .map(([id, name, flags]) => {
                    const ruler = Boolean(flags & 1);
                    const ambassador = Boolean(flags & 4);
                    return {
                        label: ruler ? `*${name}*` : ambassador ? `#${name}#` : name,
                        value: id,
                        disabled: permission === 4 && ambassador && nation.mailbox !== ownMailbox,
                        color: nation.color,
                    };
                });
            if (options.length > 0) {
                groups.push({
                    label: nation.name,
                    color: nation.color,
                    options,
                });
            }
        }
        return groups;
    });

    const statusLine = computed(() => {
        if (!lobbyInfo.value) {
            return '상태 정보를 불러오는 중';
        }
        return `${lobbyInfo.value.year}년 ${lobbyInfo.value.month}월 · 턴 ${lobbyInfo.value.turnTerm}분`;
    });

    const realtimeLabel = computed(() => {
        if (!realtimeEnabled.value) {
            return '끔';
        }
        return realtimeStatus.value === 'connected' ? '연결됨' : '대기중';
    });

    const setRealtimeEnabled = (enabled: boolean) => {
        const wasEnabled = realtimeEnabled.value;
        realtimeEnabled.value = enabled;
        if (!enabled) {
            realtimeStatus.value = 'paused';
            reconcileRealtimeCoordinator();
            return;
        }
        if (!wasEnabled) {
            void refreshQueue.request().finally(() => reconcileRealtimeCoordinator());
        }
    };

    const updateFrontStatus = (nextStatus: FrontStatus) => {
        frontStatus.value = structurallyShare(frontStatus.value, nextStatus);
        const latestVote = nextStatus.latestVote;
        if (!latestVote || latestVote.hasVoted || typeof window === 'undefined') {
            surveyNotice.value = null;
            return;
        }
        // Ref scopes this cursor with the reset-specific UniqueConst::$serverID.
        // A profile name is stable across seasons, while vote IDs restart after reset.
        const serverId = nextStatus.serverId;
        const storageKey = `state.${serverId}.lastVote`;
        const lastSeenVoteId = Number.parseInt(window.localStorage.getItem(storageKey) ?? '0', 10);
        if (latestVote.id <= (Number.isFinite(lastSeenVoteId) ? lastSeenVoteId : 0)) {
            // Initial mounting, general creation and realtime invalidation can fetch
            // the same front status in quick succession. The first response records
            // the cursor; a duplicate response must not immediately dismiss the
            // notice that response just opened.
            if (surveyNotice.value?.id !== latestVote.id) {
                surveyNotice.value = null;
            }
            return;
        }
        window.localStorage.setItem(storageKey, latestVote.id.toString());
        surveyNotice.value = latestVote;
    };

    const dismissSurveyNotice = () => {
        surveyNotice.value = null;
    };

    const reconcilePrivateMessageNotice = (nextMessages: MessageBundle | null) => {
        const viewerGeneralId = general.value?.id;
        if (!nextMessages || !viewerGeneralId) {
            privateMessageNotice.value = null;
            return;
        }
        const newestIncomingId = nextMessages.private
            .filter((message) => message.src.generalId !== viewerGeneralId)
            .reduce((latest, message) => Math.max(latest, message.id), 0);
        const latestReadId = nextMessages.latestRead.private;
        if (dismissedPrivateMessageId <= latestReadId) dismissedPrivateMessageId = 0;
        if (newestIncomingId <= latestReadId || newestIncomingId <= dismissedPrivateMessageId) {
            privateMessageNotice.value = null;
            return;
        }
        if (privateMessageNotice.value?.messageId !== newestIncomingId) {
            privateMessageNotice.value = { messageId: newestIncomingId };
        }
    };

    const dismissPrivateMessageNotice = () => {
        dismissedPrivateMessageId = Math.max(dismissedPrivateMessageId, privateMessageNotice.value?.messageId ?? 0);
        privateMessageNotice.value = null;
    };

    const mergeRecentRecords = (current: RecentRecord[], incoming: RecentRecord[]): RecentRecord[] => {
        const merged = new Map(current.map((entry) => [entry.id, entry]));
        for (const entry of incoming) {
            merged.set(entry.id, entry);
        }
        return [...merged.values()].sort((left, right) => right.id - left.id).slice(0, 15);
    };

    const resetRecentRecords = (id: number | null) => {
        globalRecords.value = [];
        generalRecords.value = [];
        worldHistory.value = [];
        lastGeneralRecordId = 0;
        lastWorldHistoryId = 0;
        recordGeneralId = id;
        frontStatus.value = null;
        surveyNotice.value = null;
        privateMessageNotice.value = null;
        dismissedPrivateMessageId = 0;
    };

    const applyRecentRecords = (records: Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>) => {
        globalRecords.value = structurallyShare(
            globalRecords.value,
            mergeRecentRecords(globalRecords.value, records.global)
        );
        generalRecords.value = structurallyShare(
            generalRecords.value,
            mergeRecentRecords(generalRecords.value, records.general)
        );
        worldHistory.value = structurallyShare(
            worldHistory.value,
            mergeRecentRecords(worldHistory.value, records.history)
        );
        lastGeneralRecordId = Math.max(lastGeneralRecordId, records.global[0]?.id ?? 0, records.general[0]?.id ?? 0);
        lastWorldHistoryId = Math.max(lastWorldHistoryId, records.history[0]?.id ?? 0);
    };

    const applyDashboardPatch = (patch: DashboardReadModelPatch) => {
        if (patch.contextSnapshot === null) {
            contextSnapshot = null;
            commandTableSnapshot = undefined;
            boardAccessSnapshot = undefined;
            general.value = null;
            city.value = null;
            nation.value = null;
            commandTable.value = null;
            boardAccess.value = null;
            reservedGeneralTurns.value = null;
            reservedGeneralRevision.value = 0;
            reservedGeneralAutorunLimit.value = null;
            resetRecentRecords(null);
            commandTableRevision = null;
            boardAccessRevision = null;
            contextSourceRevision = null;
            commandTableSourceRevision = null;
            boardAccessSourceRevision = null;
        } else if (patch.contextSnapshot !== undefined) {
            contextSnapshot = patch.contextSnapshot;
            general.value = structurallyShare(general.value, patch.contextSnapshot.general);
            city.value = structurallyShare(city.value, patch.contextSnapshot.city);
            nation.value = structurallyShare(nation.value, patch.contextSnapshot.nation);
        }
        if (patch.general === null) {
            contextSnapshot = null;
            commandTableSnapshot = undefined;
            boardAccessSnapshot = undefined;
            general.value = null;
            city.value = null;
            nation.value = null;
            commandTable.value = null;
            boardAccess.value = null;
            reservedGeneralTurns.value = null;
            reservedGeneralRevision.value = 0;
            reservedGeneralAutorunLimit.value = null;
            resetRecentRecords(null);
            contextRevision = null;
            commandTableRevision = null;
            boardAccessRevision = null;
            contextSourceRevision = null;
            commandTableSourceRevision = null;
            boardAccessSourceRevision = null;
        } else if (patch.general !== undefined) {
            general.value = structurallyShare(general.value, patch.general);
        }
        if (patch.city !== undefined) city.value = structurallyShare(city.value, patch.city);
        if (patch.nation !== undefined) nation.value = structurallyShare(nation.value, patch.nation);
        if (patch.lobbyInfo !== undefined) lobbyInfo.value = structurallyShare(lobbyInfo.value, patch.lobbyInfo);
        if (patch.worldMap !== undefined) worldMap.value = structurallyShare(worldMap.value, patch.worldMap);
        if (patch.mapLayout !== undefined) mapLayout.value = structurallyShare(mapLayout.value, patch.mapLayout);
        if (patch.commandTable !== undefined) {
            commandTableSnapshot = patch.commandTable ?? undefined;
            commandTable.value = structurallyShare(commandTable.value, patch.commandTable);
        }
        if (patch.messages !== undefined) {
            messages.value = structurallyShare(messages.value, patch.messages);
            reconcilePrivateMessageNotice(messages.value);
        } else if (patch.contextSnapshot !== undefined || patch.general !== undefined) {
            // Main data is fetched concurrently, so the message bundle can arrive
            // before the authenticated general context needed to identify senders.
            reconcilePrivateMessageNotice(messages.value);
        }
        if (patch.messageContacts !== undefined) {
            messageContacts.value = structurallyShare(messageContacts.value, patch.messageContacts);
        }
        if (patch.boardAccess !== undefined) {
            boardAccessSnapshot = patch.boardAccess ?? undefined;
            boardAccess.value = structurallyShare(boardAccess.value, patch.boardAccess);
        }
        if (patch.reservedGeneralTurns !== undefined) {
            reservedGeneralTurns.value = structurallyShare<unknown>(
                reservedGeneralTurns.value,
                patch.reservedGeneralTurns
            ) as ReservedTurnView[] | null;
        }
        if (patch.reservedGeneralRevision !== undefined) {
            reservedGeneralRevision.value = patch.reservedGeneralRevision;
        }
        if (patch.reservedGeneralAutorunLimit !== undefined) {
            reservedGeneralAutorunLimit.value = patch.reservedGeneralAutorunLimit;
        }
        if (patch.globalRecords !== undefined) {
            globalRecords.value = structurallyShare(globalRecords.value, patch.globalRecords);
            lastGeneralRecordId = Math.max(lastGeneralRecordId, patch.globalRecords[0]?.id ?? 0);
        }
        if (patch.generalRecords !== undefined) {
            generalRecords.value = structurallyShare(generalRecords.value, patch.generalRecords);
            lastGeneralRecordId = Math.max(lastGeneralRecordId, patch.generalRecords[0]?.id ?? 0);
        }
        if (patch.worldHistory !== undefined) {
            worldHistory.value = structurallyShare(worldHistory.value, patch.worldHistory);
            lastWorldHistoryId = Math.max(lastWorldHistoryId, patch.worldHistory[0]?.id ?? 0);
        }
        if (patch.frontStatus === null) {
            frontStatus.value = null;
            surveyNotice.value = null;
        } else if (patch.frontStatus !== undefined) {
            updateFrontStatus(patch.frontStatus);
        }
        if (patch.tournamentStage !== undefined) {
            tournamentStage.value = patch.tournamentStage;
        }
        if (patch.tournamentType !== undefined) {
            tournamentType.value = patch.tournamentType;
        }
        if (patch.contextRevision !== undefined) {
            contextRevision = patch.contextRevision;
            contextSourceRevision = patch.contextSourceRevision ?? null;
        } else if (patch.contextSourceRevision !== undefined) {
            contextSourceRevision = patch.contextSourceRevision;
        }
        if (patch.commandTableRevision !== undefined) {
            commandTableRevision = patch.commandTableRevision;
            commandTableSourceRevision = patch.commandTableSourceRevision ?? null;
        } else if (patch.commandTableSourceRevision !== undefined) {
            commandTableSourceRevision = patch.commandTableSourceRevision;
        }
        if (patch.boardAccessRevision !== undefined) {
            boardAccessRevision = patch.boardAccessRevision;
            boardAccessSourceRevision = patch.boardAccessSourceRevision ?? null;
        } else if (patch.boardAccessSourceRevision !== undefined) {
            boardAccessSourceRevision = patch.boardAccessSourceRevision;
        }
    };

    const applyTurnEngineRunning = (turnEngineRunning: boolean | null | undefined) => {
        if (turnEngineRunning === undefined || !lobbyInfo.value) return;
        lobbyInfo.value = structurallyShare(lobbyInfo.value, {
            ...lobbyInfo.value,
            turnEngineRunning,
        });
    };

    const currentDashboardPatch = (): DashboardReadModelPatch => {
        const patch: DashboardReadModelPatch = {};
        patch.contextSnapshot = toRaw(contextSnapshot);
        patch.contextRevision = contextRevision;
        patch.commandTableRevision = commandTableRevision;
        patch.boardAccessRevision = boardAccessRevision;
        patch.contextSourceRevision = contextSourceRevision;
        patch.commandTableSourceRevision = commandTableSourceRevision;
        patch.boardAccessSourceRevision = boardAccessSourceRevision;
        patch.general = toRaw(general.value);
        patch.city = toRaw(city.value);
        patch.nation = toRaw(nation.value);
        patch.lobbyInfo = toRaw(lobbyInfo.value);
        patch.worldMap = toRaw(worldMap.value);
        patch.mapLayout = toRaw(mapLayout.value);
        patch.commandTable = commandTableSnapshot ?? null;
        patch.messages = toRaw(messages.value);
        patch.messageContacts = toRaw(messageContacts.value);
        patch.boardAccess = boardAccessSnapshot ?? null;
        patch.reservedGeneralTurns = toRaw(reservedGeneralTurns.value as unknown) as ReservedTurnView[] | null;
        patch.reservedGeneralRevision = reservedGeneralRevision.value;
        patch.reservedGeneralAutorunLimit = reservedGeneralAutorunLimit.value;
        patch.globalRecords = toRaw(globalRecords.value);
        patch.generalRecords = toRaw(generalRecords.value);
        patch.worldHistory = toRaw(worldHistory.value);
        patch.frontStatus = toRaw(frontStatus.value);
        patch.tournamentStage = tournamentStage.value;
        patch.tournamentType = tournamentType.value;
        return patch;
    };

    const resolveContextBundlePatch = (bundle: ContextBundleDelta): DashboardReadModelPatch => {
        const patch: DashboardReadModelPatch = {};

        if (bundle.context) {
            const applied = applyReadModelDelta(contextSnapshot, contextRevision, bundle.context);
            patch.contextRevision = applied.revision;
            patch.contextSourceRevision = applied.sourceRevision ?? null;
            if (bundle.context.kind !== 'unchanged') {
                patch.contextSnapshot = applied.data;
            }
        }
        if (bundle.commandTable) {
            const applied = applyReadModelDelta(commandTableSnapshot, commandTableRevision, bundle.commandTable);
            patch.commandTableRevision = applied.revision;
            patch.commandTableSourceRevision = applied.sourceRevision ?? null;
            if (bundle.commandTable.kind !== 'unchanged') {
                patch.commandTable = applied.data;
            }
        }
        if (bundle.boardAccess) {
            const applied = applyReadModelDelta(boardAccessSnapshot, boardAccessRevision, bundle.boardAccess);
            patch.boardAccessRevision = applied.revision;
            patch.boardAccessSourceRevision = applied.sourceRevision ?? null;
            if (bundle.boardAccess.kind !== 'unchanged') {
                patch.boardAccess = applied.data;
            }
        }

        return patch;
    };

    const fetchContextBundlePatch = async (
        include: DashboardContextBundleInclude,
        forceSnapshot = false,
        refreshGrant?: string
    ): Promise<DashboardReadModelPatch> => {
        const queryOptions = createRealtimeRequestOptions(refreshGrant);
        const request = (force: boolean) =>
            trpc.dashboard.getContextBundleDelta.query(
                {
                    include,
                    known: force
                        ? undefined
                        : {
                              ...(contextRevision ? { context: contextRevision } : {}),
                              ...(commandTableRevision ? { commandTable: commandTableRevision } : {}),
                              ...(boardAccessRevision ? { boardAccess: boardAccessRevision } : {}),
                          },
                    knownSource: force
                        ? undefined
                        : {
                              ...(contextSourceRevision ? { context: contextSourceRevision } : {}),
                              ...(commandTableSourceRevision ? { commandTable: commandTableSourceRevision } : {}),
                              ...(boardAccessSourceRevision ? { boardAccess: boardAccessSourceRevision } : {}),
                          },
                    forceSnapshot: force || undefined,
                },
                queryOptions
            );

        return resolveWithReadModelSnapshotFallback({
            request,
            resolve: resolveContextBundlePatch,
            forceSnapshot,
        });
    };

    const refreshMainData = async () => {
        const isInitialLoad = !initialized;
        if (isInitialLoad) {
            loading.value = true;
        } else {
            refreshing.value = true;
        }
        error.value = null;
        recordsError.value = null;
        frontStatusError.value = null;

        try {
            const contextPatch = await fetchContextBundlePatch(
                { context: true, commandTable: true, boardAccess: true },
                true
            );
            accessLimited.value = false;
            applyDashboardPatch(contextPatch);
            const context = contextSnapshot;

            if (!context) {
                initialized = true;
                return;
            }

            const id = context.general.id;
            if (recordGeneralId !== id) {
                resetRecentRecords(id);
            }
            const layoutPromise = mapLayout.value ? Promise.resolve(mapLayout.value) : trpc.world.getMapLayout.query();
            const generalTurnsPromise = trpc.turns.reserved.getGeneral.query({ generalId: id });
            const recordsPromise = trpc.general.getRecentRecords
                .query({
                    lastGeneralRecordId,
                    lastWorldHistoryId,
                })
                .catch((err: unknown) => {
                    recordsError.value = resolveErrorMessage(err);
                    return null;
                });
            const frontStatusPromise = trpc.general.getFrontStatus.query().catch((err: unknown) => {
                frontStatusError.value = resolveErrorMessage(err);
                return null;
            });
            const tournamentPromise = trpc.tournament.getState.query().catch(() => undefined);
            const [layout, lobby, map, messageData, contacts, generalTurns, records, nextFrontStatus, tournamentState] =
                await Promise.all([
                    layoutPromise,
                    trpc.lobby.info.query(),
                    trpc.world.getMap.query({ generalId: id, showMe: true, useCache: true }),
                    trpc.messages.getRecent.query({ generalId: id }),
                    trpc.messages.getContacts.query({ generalId: id }),
                    generalTurnsPromise,
                    recordsPromise,
                    frontStatusPromise,
                    tournamentPromise,
                ]);

            general.value = structurallyShare(general.value, context.general);
            city.value = structurallyShare(city.value, context.city);
            nation.value = structurallyShare(nation.value, context.nation);
            mapLayout.value = structurallyShare(mapLayout.value, layout);
            lobbyInfo.value = structurallyShare(lobbyInfo.value, lobby);
            worldMap.value = structurallyShare(worldMap.value, map);
            messages.value = structurallyShare(messages.value, messageData);
            reconcilePrivateMessageNotice(messages.value);
            messageContacts.value = structurallyShare(messageContacts.value, contacts);
            reservedGeneralTurns.value = structurallyShare<unknown>(
                reservedGeneralTurns.value,
                generalTurns.turns
            ) as ReservedTurnView[];
            reservedGeneralRevision.value = generalTurns.revision;
            reservedGeneralAutorunLimit.value = generalTurns.autorunLimit ?? null;
            if (records) {
                applyRecentRecords(records);
            }
            if (nextFrontStatus) {
                updateFrontStatus(nextFrontStatus);
            }
            if (tournamentState !== undefined) {
                tournamentStage.value = tournamentState?.stage ?? 0;
                tournamentType.value = tournamentState?.type ?? null;
            }
            if (initializedMailboxGeneralId !== id) {
                targetMailbox.value = MESSAGE_MAILBOX_NATIONAL_BASE + context.general.nationId;
                initializedMailboxGeneralId = id;
            }
            initialized = true;
        } catch (err) {
            handleDashboardError(err);
        } finally {
            if (isInitialLoad) {
                loading.value = false;
            } else {
                refreshing.value = false;
            }
        }
    };

    const refreshQueue = createLatestRefreshQueue(refreshMainData);
    const loadMainData = () => refreshQueue.request();
    let realtimeCoordinator: BroadcastTabCoordinator<DashboardTabMessage> | null = null;
    let realtimeCoordinatorScope: string | null = null;

    const publishDashboardPatch = (patch: DashboardReadModelPatch) => {
        if (!realtimeCoordinator) {
            return;
        }
        // BroadcastChannel uses the structured-clone algorithm. Normalize the
        // full payload so nested Vue proxies never reach that browser boundary.
        try {
            realtimeCoordinator.postFromLeader({ kind: 'patch', patch: cloneReadModelJson(patch) });
        } catch {
            // Same-account fan-out is best effort; the leader's local refresh
            // and the next visibility/full-snapshot recovery remain valid.
        }
    };

    const realtimeRefreshQueue = createRateLimitedRefreshQueue(
        async () => {
            await refreshQueue.request();
            publishDashboardPatch(currentDashboardPatch());
        },
        {
            minIntervalMs: REALTIME_FULL_REFRESH_MIN_INTERVAL_MS,
        }
    );

    const refreshChangedReadModels = async (plan: RealtimeReadModelInvalidation, refreshGrant: string) => {
        const id = generalId.value;
        if (!id) {
            return;
        }
        if (!Object.values(plan).some(Boolean)) {
            return;
        }

        refreshing.value = true;
        error.value = null;
        if (plan.records) recordsError.value = null;
        if (plan.frontStatus) frontStatusError.value = null;
        try {
            const queryOptions = createRealtimeRequestOptions(refreshGrant);
            // Every automatic refresh crosses this access-limit gate before
            // any selected follow-up query starts. An all-false bundle is an
            // access-only check and does not project general context.
            const contextPatch = await fetchContextBundlePatch(
                resolveDashboardContextBundleInclude(plan),
                false,
                refreshGrant
            );
            accessLimited.value = false;
            const lobbyPromise = plan.lobby
                ? trpc.lobby.info.query(undefined, queryOptions)
                : Promise.resolve(undefined);
            const mapPromise = plan.map
                ? trpc.world.getMap.query({ generalId: id, showMe: true, useCache: true }, queryOptions)
                : Promise.resolve(undefined);
            const contactsPromise = plan.contacts
                ? trpc.messages.getContacts.query({ generalId: id }, queryOptions)
                : Promise.resolve(undefined);
            const reservedPromise = plan.reservedTurns
                ? trpc.turns.reserved.getGeneral.query({ generalId: id }, queryOptions)
                : Promise.resolve(undefined);
            const recordsPromise = plan.records
                ? trpc.general.getRecentRecords
                      .query({ lastGeneralRecordId, lastWorldHistoryId }, queryOptions)
                      .catch((err: unknown) => {
                          recordsError.value = resolveErrorMessage(err);
                          return null;
                      })
                : Promise.resolve(undefined);
            const frontPromise = plan.frontStatus
                ? trpc.general.getFrontStatus.query(undefined, queryOptions).catch((err: unknown) => {
                      frontStatusError.value = resolveErrorMessage(err);
                      return null;
                  })
                : Promise.resolve(undefined);
            const tournamentPromise: Promise<TournamentState | undefined> = plan.tournament
                ? trpc.tournament.getState.query(undefined, queryOptions).catch(() => undefined)
                : Promise.resolve(undefined);

            const [lobby, map, contacts, generalTurns, records, nextFrontStatus, tournamentState] = await Promise.all([
                lobbyPromise,
                mapPromise,
                contactsPromise,
                reservedPromise,
                recordsPromise,
                frontPromise,
                tournamentPromise,
            ]);

            const patch: DashboardReadModelPatch = { ...contextPatch };
            if (lobby !== undefined) patch.lobbyInfo = lobby;
            if (map !== undefined) patch.worldMap = map;
            if (contacts !== undefined) patch.messageContacts = contacts;
            if (generalTurns !== undefined) {
                patch.reservedGeneralTurns = generalTurns.turns;
                patch.reservedGeneralRevision = generalTurns.revision;
                patch.reservedGeneralAutorunLimit = generalTurns.autorunLimit ?? null;
            }
            if (records) {
                const nextGlobalRecords = mergeRecentRecords(globalRecords.value, records.global);
                const nextGeneralRecords = mergeRecentRecords(generalRecords.value, records.general);
                const nextWorldHistory = mergeRecentRecords(worldHistory.value, records.history);
                patch.globalRecords = nextGlobalRecords;
                patch.generalRecords = nextGeneralRecords;
                patch.worldHistory = nextWorldHistory;
            }
            if (nextFrontStatus) patch.frontStatus = nextFrontStatus;
            if (tournamentState !== undefined) {
                patch.tournamentStage = tournamentState?.stage ?? 0;
                patch.tournamentType = tournamentState?.type ?? null;
            }
            applyDashboardPatch(patch);
            publishDashboardPatch(patch);
        } catch (err) {
            handleDashboardError(err);
        } finally {
            refreshing.value = false;
        }
    };

    const readModelRefreshQueue = createMergedReadModelRefreshQueue(refreshChangedReadModels);

    const refreshMessages = async (refreshGrant?: string) => {
        const id = generalId.value;
        if (!id) {
            return;
        }
        try {
            const nextMessages = await trpc.messages.getRecent.query(
                { generalId: id },
                createRealtimeRequestOptions(refreshGrant)
            );
            const patch = { messages: nextMessages } satisfies DashboardReadModelPatch;
            applyDashboardPatch(patch);
            publishDashboardPatch(patch);
        } catch (err) {
            handleDashboardError(err);
        }
    };

    const sendMessage = async () => {
        const id = generalId.value;
        if (!id) {
            return;
        }
        const mailbox = targetMailbox.value;
        const text = messageDraftText.value.trim();
        if (!text) {
            return;
        }
        if (mailbox <= 0) {
            return;
        }

        try {
            messageDraftText.value = '';
            await trpc.messages.send.mutate({
                generalId: id,
                mailbox,
                text,
            });
            await refreshMessages();
        } catch (err) {
            error.value = resolveErrorMessage(err);
        }
    };

    const loadOlderMessages = async (type: MessageType) => {
        const id = generalId.value;
        if (!id || !messages.value) {
            return;
        }

        const bucket = messages.value[type] ?? [];
        const oldest = bucket[bucket.length - 1];
        if (!oldest) {
            return;
        }

        try {
            const older = await trpc.messages.getOld.query({
                generalId: id,
                type,
                to: oldest.id,
            });
            const merged = {
                ...messages.value,
                [type]: [...bucket, ...older[type]],
            } as MessageBundle;
            messages.value = merged;
        } catch (err) {
            error.value = resolveErrorMessage(err);
        }
    };

    const respondToMessage = async (messageId: number, response: boolean) => {
        const id = generalId.value;
        if (!id) {
            return;
        }
        try {
            const result = await trpc.messages.respond.mutate({
                generalId: id,
                messageId,
                response,
            });
            if (!result.result) {
                error.value = result.reason;
            }
            await refreshMessages();
        } catch (err) {
            error.value = resolveErrorMessage(err);
        }
    };

    const readLatestMessage = async (type: 'private' | 'diplomacy', messageId: number): Promise<boolean> => {
        const id = generalId.value;
        if (!id || messageId <= 0) {
            return false;
        }
        try {
            await trpc.messages.readLatest.mutate({
                generalId: id,
                type,
                messageId,
            });
            if (messages.value) {
                messages.value = {
                    ...messages.value,
                    latestRead: {
                        ...messages.value.latestRead,
                        [type]: Math.max(messages.value.latestRead[type], messageId),
                    },
                };
                reconcilePrivateMessageNotice(messages.value);
            }
            return true;
        } catch (err) {
            error.value = resolveErrorMessage(err);
            return false;
        }
    };

    const acknowledgePrivateMessageNotice = async (): Promise<boolean> => {
        const messageId = privateMessageNotice.value?.messageId;
        if (!messageId) return false;
        return readLatestMessage('private', messageId);
    };

    const deleteMessage = async (messageId: number) => {
        const id = generalId.value;
        if (!id) {
            return;
        }
        try {
            await trpc.messages.delete.mutate({ generalId: id, messageId });
            await refreshMessages();
        } catch (err) {
            error.value = resolveErrorMessage(err);
        }
    };

    const setGeneralTurn = async (turnIndex: number, action: string, args: Record<string, unknown> = {}) => {
        const id = generalId.value;
        if (!id) {
            return;
        }
        try {
            const result = await trpc.turns.reserved.setGeneral.mutate({
                generalId: id,
                turnIndex,
                action,
                args,
                expectedRevision: reservedGeneralRevision.value,
            });
            reservedGeneralTurns.value = result.turns;
            reservedGeneralRevision.value = result.revision;
            reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
        } catch (err) {
            error.value = resolveErrorMessage(err);
            const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
            if (snapshot) {
                reservedGeneralTurns.value = snapshot.turns;
                reservedGeneralRevision.value = snapshot.revision;
                reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
            }
        }
    };

    const shiftGeneralTurns = async (amount: number) => {
        const id = generalId.value;
        if (!id) {
            return;
        }
        try {
            const result = await trpc.turns.reserved.shiftGeneral.mutate({
                generalId: id,
                amount,
                expectedRevision: reservedGeneralRevision.value,
            });
            reservedGeneralTurns.value = result.turns;
            reservedGeneralRevision.value = result.revision;
            reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
        } catch (err) {
            error.value = resolveErrorMessage(err);
            const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
            if (snapshot) {
                reservedGeneralTurns.value = snapshot.turns;
                reservedGeneralRevision.value = snapshot.revision;
                reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
            }
        }
    };

    const setGeneralTurns = async (
        entries: Array<{ turnList: number[]; action: string; args: Record<string, unknown> }>
    ) => {
        const id = generalId.value;
        if (!id || !entries.length) return;
        try {
            const result = await trpc.turns.reserved.setGeneralBulk.mutate({
                generalId: id,
                entries,
                expectedRevision: reservedGeneralRevision.value,
            });
            reservedGeneralTurns.value = result.turns;
            reservedGeneralRevision.value = result.revision;
            reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
        } catch (err) {
            error.value = resolveErrorMessage(err);
            const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
            if (snapshot) {
                reservedGeneralTurns.value = snapshot.turns;
                reservedGeneralRevision.value = snapshot.revision;
                reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
            }
        }
    };

    const repeatGeneralTurns = async (amount: number) => {
        const id = generalId.value;
        if (!id) return;
        try {
            const result = await trpc.turns.reserved.repeatGeneral.mutate({
                generalId: id,
                amount,
                expectedRevision: reservedGeneralRevision.value,
            });
            reservedGeneralTurns.value = result.turns;
            reservedGeneralRevision.value = result.revision;
            reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
        } catch (err) {
            error.value = resolveErrorMessage(err);
            const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
            if (snapshot) {
                reservedGeneralTurns.value = snapshot.turns;
                reservedGeneralRevision.value = snapshot.revision;
                reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
            }
        }
    };

    let realtimeSource: EventSource | null = null;
    let realtimeToken: string | null = null;
    let visibilityListenerInstalled = false;

    const isAccessToken = (token: string | null): boolean => Boolean(token?.startsWith('ga_'));

    const buildRealtimeUrl = (token: string): string => {
        const base = gameFrontendRuntimeConfig.gameSseUrl;
        const url = new URL(base, window.location.origin);
        url.searchParams.set('token', token);
        return url.toString();
    };

    const parseRealtimePayload = (raw: MessageEvent): PublicRealtimeEvent | null => {
        if (!raw.data || typeof raw.data !== 'string') {
            return null;
        }
        try {
            const parsed = JSON.parse(raw.data) as PublicRealtimeEvent;
            if (!parsed || typeof parsed !== 'object') {
                return null;
            }
            if (typeof parsed.type !== 'string') {
                return null;
            }
            return parsed;
        } catch {
            return null;
        }
    };

    const closeRealtimeSource = () => {
        if (!realtimeSource) {
            return;
        }
        realtimeSource.close();
        realtimeSource = null;
        realtimeToken = null;
    };

    const isRealtimeParticipant = (): boolean =>
        realtimeActive.value &&
        document.visibilityState !== 'hidden' &&
        realtimeEnabled.value &&
        !accessLimited.value &&
        session.isReady &&
        session.hasGeneral &&
        generalId.value !== null;

    const closeRealtimeCoordinator = () => {
        const coordinator = realtimeCoordinator;
        realtimeCoordinator = null;
        realtimeCoordinatorScope = null;
        coordinator?.stop();
        closeRealtimeSource();
    };

    const reconcileRealtimeCoordinator = () => {
        if (typeof window === 'undefined') return;
        if (!isRealtimeParticipant()) {
            closeRealtimeCoordinator();
            return;
        }
        if (typeof BroadcastChannel === 'undefined') {
            void connectRealtime();
            return;
        }

        const profile = session.profile ?? 'game';
        const account = session.user?.id ?? `general-${generalId.value}`;
        // Do not let a tab that survived a reset exchange stale read-model patches
        // with tabs belonging to the next season of the same profile.
        const serverId = frontStatus.value?.serverId ?? profile;
        const scope = `${encodeURIComponent(serverId)}:${encodeURIComponent(profile)}:${encodeURIComponent(account)}`;
        if (realtimeCoordinator && realtimeCoordinatorScope === scope) return;

        closeRealtimeCoordinator();
        realtimeCoordinatorScope = scope;
        realtimeCoordinator = createBroadcastTabCoordinator<DashboardTabMessage>(`sammo-main-dashboard:${scope}`, {
            onLeadershipChange: (leader) => {
                if (leader) {
                    void connectRealtime();
                } else {
                    closeRealtimeSource();
                    if (realtimeEnabled.value) realtimeStatus.value = 'idle';
                }
            },
            onPayload: (message) => {
                if (!isRealtimeParticipant()) return;
                if (message.kind === 'patch') {
                    markGameServerContact();
                    applyDashboardPatch(message.patch);
                    return;
                }
                realtimeStatus.value = message.status;
                applyTurnEngineRunning(message.turnEngineRunning);
                if (message.status === 'connected') markGameServerContact();
            },
        });
        realtimeCoordinator.start();
    };

    const ensureAccessToken = async (): Promise<string | null> => {
        if (!session.gameToken) {
            return null;
        }
        if (isAccessToken(session.gameToken)) {
            return session.gameToken;
        }
        const exchanged = await session.exchangeGatewayToken();
        if (!exchanged) {
            return null;
        }
        return session.gameToken && isAccessToken(session.gameToken) ? session.gameToken : null;
    };

    const connectRealtime = async () => {
        if (typeof window === 'undefined') {
            return;
        }
        if (
            !realtimeActive.value ||
            document.visibilityState === 'hidden' ||
            !realtimeEnabled.value ||
            !session.isReady ||
            !session.hasGeneral ||
            (realtimeCoordinator !== null && !realtimeCoordinator.isLeader())
        ) {
            return;
        }
        const token = await ensureAccessToken();
        if (!token) {
            realtimeStatus.value = 'idle';
            return;
        }
        if (!isRealtimeParticipant() || (realtimeCoordinator !== null && !realtimeCoordinator.isLeader())) {
            return;
        }
        if (realtimeSource && realtimeToken === token) {
            return;
        }
        closeRealtimeSource();
        realtimeToken = token;
        realtimeStatus.value = 'idle';

        const source = new EventSource(buildRealtimeUrl(token));
        realtimeSource = source;

        source.addEventListener('open', () => {
            markGameServerContact();
            realtimeStatus.value = 'connected';
            realtimeCoordinator?.postFromLeader({ kind: 'status', status: 'connected' });
        });
        source.addEventListener('error', () => {
            realtimeStatus.value = realtimeEnabled.value ? 'idle' : 'paused';
            realtimeCoordinator?.postFromLeader({ kind: 'status', status: 'idle' });
        });
        source.addEventListener('readModelInvalidated', (event) => {
            if (realtimeCoordinator !== null && !realtimeCoordinator.isLeader()) return;
            const payload = parseRealtimePayload(event);
            if (!payload || payload.type !== 'readModelInvalidated') {
                return;
            }
            markGameServerContact();
            readModelRefreshQueue.request(payload.invalidation, payload.refreshGrant);
        });
        source.addEventListener('messagesInvalidated', (event) => {
            if (realtimeCoordinator !== null && !realtimeCoordinator.isLeader()) return;
            const payload = parseRealtimePayload(event);
            if (!payload || payload.type !== 'messagesInvalidated') {
                return;
            }
            markGameServerContact();
            void refreshMessages(payload.refreshGrant);
        });

        // Rolling deployment fallback: an older API may still expose internal
        // events. Do not inspect their payload; use the bounded full refresh.
        for (const legacyEventType of ['turnCompleted', 'readModelChanged'] as const) {
            source.addEventListener(legacyEventType, () => {
                if (realtimeCoordinator !== null && !realtimeCoordinator.isLeader()) return;
                markGameServerContact();
                realtimeRefreshQueue.request();
            });
        }
        source.addEventListener('messageCreated', () => {
            if (realtimeCoordinator !== null && !realtimeCoordinator.isLeader()) return;
            markGameServerContact();
            void refreshMessages();
        });
        source.addEventListener('ping', (event) => {
            let turnEngineRunning: boolean | null | undefined;
            if (event instanceof MessageEvent && typeof event.data === 'string') {
                try {
                    const payload = JSON.parse(event.data) as { turnEngineRunning?: unknown };
                    if (typeof payload.turnEngineRunning === 'boolean' || payload.turnEngineRunning === null) {
                        turnEngineRunning = payload.turnEngineRunning;
                    }
                } catch {
                    // Older APIs send an empty heartbeat. Keep the last explicit engine state.
                }
            }
            applyTurnEngineRunning(turnEngineRunning);
            markGameServerContact();
            if (realtimeEnabled.value) {
                realtimeStatus.value = 'connected';
                realtimeCoordinator?.postFromLeader({
                    kind: 'status',
                    status: 'connected',
                    ...(turnEngineRunning === undefined ? {} : { turnEngineRunning }),
                });
            }
        });
    };

    const handleVisibilityChange = () => {
        if (!realtimeActive.value) return;
        if (document.visibilityState === 'hidden') {
            realtimeRefreshQueue.cancelPending();
            readModelRefreshQueue.cancelPending();
            closeRealtimeCoordinator();
            realtimeStatus.value = 'idle';
            return;
        }
        realtimeRefreshQueue.beginCooldown();
        void refreshQueue.request().finally(() => reconcileRealtimeCoordinator());
    };

    const handleGameServerReconnected = () => {
        if (!realtimeActive.value || document.visibilityState === 'hidden') return;
        realtimeRefreshQueue.beginCooldown();
        void refreshQueue.request().finally(() => reconcileRealtimeCoordinator());
    };

    const startRealtime = () => {
        if (typeof window === 'undefined' || realtimeActive.value) return;
        realtimeActive.value = true;
        realtimeRefreshQueue.beginCooldown();
        if (!visibilityListenerInstalled) {
            document.addEventListener('visibilitychange', handleVisibilityChange);
            window.addEventListener(GAME_SERVER_RECONNECTED_EVENT, handleGameServerReconnected);
            visibilityListenerInstalled = true;
        }
        reconcileRealtimeCoordinator();
    };

    const stopRealtime = () => {
        realtimeActive.value = false;
        realtimeRefreshQueue.cancelPending();
        readModelRefreshQueue.cancelPending();
        closeRealtimeCoordinator();
        if (visibilityListenerInstalled) {
            document.removeEventListener('visibilitychange', handleVisibilityChange);
            window.removeEventListener(GAME_SERVER_RECONNECTED_EVENT, handleGameServerReconnected);
            visibilityListenerInstalled = false;
        }
        realtimeStatus.value = realtimeEnabled.value ? 'idle' : 'paused';
    };

    watch(
        () => [
            realtimeActive.value,
            realtimeEnabled.value,
            session.isReady,
            session.hasGeneral,
            session.gameToken,
            session.profile,
            session.user?.id,
            generalId.value,
            accessLimited.value,
            frontStatus.value?.serverId,
        ],
        ([active, enabled, ready, hasGeneral, , , , , limited]) => {
            realtimeStatus.value = !enabled || limited ? 'paused' : realtimeStatus.value;
            if (!active || !ready || !hasGeneral) {
                realtimeStatus.value = enabled && !limited ? 'idle' : 'paused';
            }
            reconcileRealtimeCoordinator();
        }
    );

    return {
        loading,
        refreshing,
        error,
        recordsError,
        frontStatusError,
        realtimeEnabled,
        realtimeStatus,
        general,
        city,
        nation,
        lobbyInfo,
        worldMap,
        mapLayout,
        selectedCity,
        commandTable,
        messages,
        messageContacts,
        boardAccess,
        reservedGeneralTurns,
        reservedGeneralAutorunLimit,
        globalRecords,
        generalRecords,
        worldHistory,
        frontStatus,
        tournamentStage,
        tournamentType,
        surveyNotice,
        privateMessageNotice,
        messageDraftText,
        targetMailbox,
        mailboxGroups,
        statusLine,
        realtimeLabel,
        setRealtimeEnabled,
        startRealtime,
        stopRealtime,
        dismissSurveyNotice,
        dismissPrivateMessageNotice,
        acknowledgePrivateMessageNotice,
        loadMainData,
        refreshMessages,
        sendMessage,
        loadOlderMessages,
        respondToMessage,
        readLatestMessage,
        deleteMessage,
        setGeneralTurn,
        setGeneralTurns,
        shiftGeneralTurns,
        repeatGeneralTurns,
    };
});
