/**
 * graph.js — In-browser Engram graph for el-ui state management.
 *
 * Every piece of component state is a node in this graph.
 * Edges encode relationships between state items (e.g., derived values,
 * related data, component hierarchies).
 *
 * Reactivity is driven by spreading activation: when a node is updated,
 * activation spreads outward through edges, and subscribers on any activated
 * node are notified. Only subscribers in the activation surface update —
 * this is the core of el-ui's selective re-rendering.
 */

export class Graph {
    constructor() {
        /** @type {Map<string, {id: string, type: string, name: string, content: any, importance: number, edges: string[]}>} */
        this.nodes = new Map();

        /** @type {Map<string, {id: string, from: string, to: string, weight: number, relation: string}>} */
        this.edges = new Map();

        /** @type {Map<string, Array<function>>} */
        this.subscribers = new Map();
    }

    /**
     * Create a new node and return its ID.
     * @param {{type: string, name: string, content: any, importance?: number}} opts
     * @returns {string} node ID
     */
    seed({ type, name, content, importance = 0.5 }) {
        const id = this._uuid();
        this.nodes.set(id, { id, type, name, content, importance, edges: [] });
        return id;
    }

    /**
     * Get a node by ID.
     * @param {string} id
     * @returns {{id: string, type: string, name: string, content: any, importance: number, edges: string[]} | undefined}
     */
    get(id) {
        return this.nodes.get(id);
    }

    /**
     * Update node content and trigger spreading activation.
     * This is the primary mutation point — all state changes go through here.
     * @param {string} id
     * @param {any} newContent
     */
    update(id, newContent) {
        const node = this.nodes.get(id);
        if (!node) return;

        node.content = newContent;
        // Activation boosts importance — recently-changed state nodes
        // become more salient (same model as Engram salience boost).
        node.importance = Math.min(1.0, node.importance + 0.1);

        // Spreading activation from the updated node
        const activated = this.activate(id);

        // Notify all subscribers in the activation surface
        for (const nodeId of activated) {
            const subs = this.subscribers.get(nodeId);
            if (subs && subs.length > 0) {
                const activatedNode = this.nodes.get(nodeId);
                if (activatedNode) {
                    subs.forEach(cb => cb(activatedNode));
                }
            }
        }

        // Always notify direct subscribers on the changed node itself
        const directSubs = this.subscribers.get(id);
        if (directSubs && directSubs.length > 0) {
            directSubs.forEach(cb => cb(node));
        }
    }

    /**
     * Spreading activation — BFS from a seed node.
     *
     * Faithfully mirrors the Engram spreading activation algorithm:
     *   strength = parent_strength × edge.weight × target.importance
     *
     * (In the browser we omit cosine_sim since we have no embedding vectors —
     * the importance factor serves as the salience filter.)
     *
     * @param {string} seedId
     * @param {number} maxDepth
     * @param {number} pruneThreshold
     * @returns {Set<string>} set of activated node IDs (including the seed)
     */
    activate(seedId, maxDepth = 3, pruneThreshold = 0.01) {
        const result = new Set([seedId]);
        /** @type {Array<{id: string, strength: number, depth: number}>} */
        const queue = [{ id: seedId, strength: 1.0, depth: 0 }];
        /** @type {Map<string, number>} */
        const bestStrength = new Map([[seedId, 1.0]]);

        while (queue.length > 0) {
            // Best-first: process highest-strength candidate
            let bestIdx = 0;
            for (let i = 1; i < queue.length; i++) {
                if (queue[i].strength > queue[bestIdx].strength) bestIdx = i;
            }
            const { id, strength, depth } = queue.splice(bestIdx, 1)[0];

            if (depth >= maxDepth) continue;

            const node = this.nodes.get(id);
            if (!node) continue;

            for (const edgeId of node.edges) {
                const edge = this.edges.get(edgeId);
                if (!edge) continue;

                const target = this.nodes.get(edge.to);
                if (!target) continue;

                // Activation formula (multiplicative — matches Engram engine)
                const targetStrength = strength * edge.weight * Math.max(0, target.importance);

                if (targetStrength <= pruneThreshold) continue;

                const prevBest = bestStrength.get(edge.to) ?? 0;
                if (targetStrength > prevBest) {
                    bestStrength.set(edge.to, targetStrength);
                    result.add(edge.to);
                    queue.push({ id: edge.to, strength: targetStrength, depth: depth + 1 });
                }
            }
        }

        return result;
    }

    /**
     * Semantic search — find nodes by content similarity.
     *
     * In v0.1 this uses simple string matching. In a future version this will
     * use embedding vectors and cosine similarity (matching the Engram core engine).
     *
     * @param {string} query
     * @param {string|null} nodeType  optional filter by node type
     * @returns {Array<{id: string, type: string, name: string, content: any, importance: number, score: number}>}
     */
    search(query, nodeType = null) {
        const results = [];
        const q = query.toLowerCase();
        for (const [, node] of this.nodes) {
            if (nodeType && node.type !== nodeType) continue;
            const content = String(node.content).toLowerCase();
            const nameMatch = node.name.toLowerCase().includes(q);
            const contentMatch = content.includes(q);
            if (nameMatch || contentMatch) {
                // Score: name matches score higher; importance boosts
                const score = (nameMatch ? 0.6 : 0) + (contentMatch ? 0.4 : 0);
                results.push({ ...node, score: score * node.importance });
            }
        }
        return results.sort((a, b) => b.score - a.score);
    }

    /**
     * Subscribe to updates on a specific node (and any activation-reachable neighbors).
     * Returns an unsubscribe function.
     *
     * @param {string} nodeId
     * @param {function} callback  called with the node object when it activates
     * @returns {function} unsubscribe
     */
    subscribe(nodeId, callback) {
        if (!this.subscribers.has(nodeId)) {
            this.subscribers.set(nodeId, []);
        }
        this.subscribers.get(nodeId).push(callback);
        return () => {
            const subs = this.subscribers.get(nodeId);
            if (!subs) return;
            const idx = subs.indexOf(callback);
            if (idx >= 0) subs.splice(idx, 1);
        };
    }

    /**
     * Connect two nodes with a directed edge.
     * Higher weight = stronger activation path.
     *
     * @param {string} fromId
     * @param {string} toId
     * @param {{weight?: number, relation?: string}} opts
     * @returns {string} edge ID
     */
    connect(fromId, toId, { weight = 1.0, relation = 'related' } = {}) {
        const edgeId = this._uuid();
        this.edges.set(edgeId, { id: edgeId, from: fromId, to: toId, weight, relation });
        const node = this.nodes.get(fromId);
        if (node) node.edges.push(edgeId);
        return edgeId;
    }

    /**
     * Return all nodes as an array (useful for debugging / DevTools).
     * @returns {Array}
     */
    dump() {
        return [...this.nodes.values()];
    }

    /** @private */
    _uuid() {
        if (typeof crypto !== 'undefined' && crypto.randomUUID) {
            return crypto.randomUUID();
        }
        // Fallback for environments without crypto.randomUUID
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
            const r = Math.random() * 16 | 0;
            return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
    }
}
