//
// Copyright 2024 DXOS.org
//
import { useEffect, useState } from 'react';
import { SpaceState } from '@dxos/client/echo';
import { log } from '@dxos/log';
import { useClient } from '@dxos/react-client';
import { useAsyncEffect } from '@dxos/react-hooks';
import { TRACE_PROCESSOR } from '@dxos/tracing';
import { DiagnosticsChannel } from '@dxos/tracing';
import { get } from '@dxos/util';
/**
 * https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Performance_data#performance_entries
 * @param entryTypes
 */
export const usePerformanceObserver = (entryTypes) => {
    const [entries, setEntries] = useState();
    useEffect(() => {
        const po = new PerformanceObserver((list) => {
            setEntries(list.getEntries());
        });
        po.observe({ entryTypes });
        return () => po.disconnect();
    }, []);
    return entries;
};
export const useStats = () => {
    const client = useClient();
    const [stats, setStats] = useState({});
    const [update, forceUpdate] = useState({});
    const performanceEntries = usePerformanceObserver([
        // https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Performance_data#performance_entries
        'first-input',
        'longtask',
        'largest-contentful-paint',
        'paint',
    ]);
    // Quick metrics.
    useAsyncEffect(async () => {
        const begin = performance.now();
        // TODO(burdon): Reconcile with diagnostics.
        const objects = TRACE_PROCESSOR.findResourcesByClassName('RepoProxy')
            .flatMap((r) => Object.values(r.instance.deref()?.handles ?? {}))
            .map((handle) => handle.doc())
            .filter(Boolean);
        const database = {
            spaces: client.spaces.get().length,
            objects: objects.length,
            documents: 0,
            documentsToReconcile: 0,
        };
        if ('measureUserAgentSpecificMemory' in window.performance) {
            // TODO(burdon): Breakdown.
            // https://developer.mozilla.org/en-US/docs/Web/API/Performance/measureUserAgentSpecificMemory
            // const { bytes } = (await (window.performance as any).measureUserAgentSpecificMemory()) as { bytes: number };
        }
        const memory = window.performance.memory;
        if (memory && typeof memory === 'object' && 'usedJSHeapSize' in memory) {
            memory.used = memory.usedJSHeapSize / memory.jsHeapSizeLimit;
        }
        log('collected stats', { elapsed: performance.now() - begin });
        setStats((stats) => Object.assign({}, stats, {
            performanceEntries,
            diagnostics: TRACE_PROCESSOR.getDiagnostics(),
            memory,
            database,
        }));
    }, [update]);
    // Slower metrics.
    useAsyncEffect(async () => {
        const begin = performance.now();
        // client.experimental.graph;
        // TODO(burdon): This is very expensive (do separately).
        const diagnostics = await client.diagnostics();
        // const s = TRACE_PROCESSOR.findResourcesByClassName('QueryState');
        const resources = get(diagnostics, 'services.diagnostics.trace.resources');
        const queries = Object.values(resources)
            .filter((res) => res.className === 'QueryState')
            .map((res) => {
            return res.info;
        });
        const syncStates = await Promise.all(client.spaces
            .get()
            .filter((space) => space.state.get() === SpaceState.SPACE_READY)
            .map((space) => space.internal.db.coreDatabase.getSyncState()));
        const documentsToReconcile = syncStates
            .flatMap((s) => s.peers?.map((p) => p.differentDocuments + p.missingOnLocal + p.missingOnRemote) ?? [])
            .reduce((acc, x) => acc + x, 0);
        log('collected stats', { elapsed: performance.now() - begin });
        setStats((stats) => Object.assign({}, stats, {
            queries,
            database: Object.assign({}, stats.database, { documentsToReconcile }),
        }));
    }, [update]);
    useEffect(() => {
        const stream = client.services.services.NetworkService.queryStatus();
        stream.subscribe((network) => {
            setStats((stats) => Object.assign({}, stats, {
                network,
            }));
        });
        return () => {
            void stream.close();
        };
    }, []);
    useEffect(() => {
        const stream = client.services.services.EdgeAgentService.queryEdgeStatus();
        stream.subscribe((edge) => {
            setStats((stats) => Object.assign({}, stats, {
                edge,
            }));
        });
        return () => {
            void stream.close();
        };
    }, []);
    const echoStatsDiagnostic = useDiagnostic({ id: 'echo-stats', instanceTag: 'shared-worker' }, 1_000);
    if (stats.database && echoStatsDiagnostic) {
        stats.database.documents = echoStatsDiagnostic.loadedDocsCount;
        stats.database.dataStats = echoStatsDiagnostic.dataStats;
    }
    return [stats, () => forceUpdate({})];
};
// TODO(burdon): Move to util.
export const removeEmpty = (obj) => {
    const maybeTruncateKey = (str) => (str.length > 32 ? str.slice(0, 8) : str);
    return Object.fromEntries(Object.entries(obj)
        .filter(([_, v]) => v !== undefined && v !== null && v !== false && !(Array.isArray(v) && v.length === 0))
        .map(([k, v]) => [k, v === Object(v) ? removeEmpty(v) : typeof v === 'string' ? maybeTruncateKey(v) : v]));
};
const useDiagnostic = (request, refreshInterval) => {
    const [data, setData] = useState();
    useEffect(() => {
        const channel = new DiagnosticsChannel();
        const fetch = async () => {
            try {
                const { data } = await channel.fetch(request);
                setData(data);
            }
            catch (error) { }
        };
        void fetch();
        const interval = setInterval(fetch, refreshInterval);
        return () => {
            clearInterval(interval);
        };
    }, []);
    return data;
};
//# sourceMappingURL=useStats.js.map