/**
 * el-ui v0.1.0 — Activation-based frontend framework.
 * State is an Engram graph. Reactivity is spreading activation.
 *
 * Bundled single-file runtime. Import this from your compiled .el components:
 *   import { Component, Graph, Renderer, Router, mount } from './el-ui.js';
 *
 * Source: runtime/src/
 * Build: cat runtime/src/graph.js runtime/src/activation.js
 *            runtime/src/renderer.js runtime/src/router.js runtime/src/index.js > dist/el-ui.js
 */

// ── graph.js ──────────────────────────────────────────────────────────────────

export class Graph {
    constructor() {
        this.nodes = new Map();
        this.edges = new Map();
        this.subscribers = new Map();
    }

    seed({ type, name, content, importance = 0.5 }) {
        const id = this._uuid();
        this.nodes.set(id, { id, type, name, content, importance, edges: [] });
        return id;
    }

    get(id) {
        return this.nodes.get(id);
    }

    update(id, newContent) {
        const node = this.nodes.get(id);
        if (!node) return;
        node.content = newContent;
        node.importance = Math.min(1.0, node.importance + 0.1);
        const activated = this.activate(id);
        for (const nodeId of activated) {
            const subs = this.subscribers.get(nodeId);
            if (subs && subs.length > 0) {
                const n = this.nodes.get(nodeId);
                if (n) subs.forEach(cb => cb(n));
            }
        }
        // Always notify direct subscribers
        const directSubs = this.subscribers.get(id);
        if (directSubs && directSubs.length > 0) {
            directSubs.forEach(cb => cb(node));
        }
    }

    activate(seedId, maxDepth = 3, pruneThreshold = 0.01) {
        const result = new Set([seedId]);
        const queue = [{ id: seedId, strength: 1.0, depth: 0 }];
        const bestStrength = new Map([[seedId, 1.0]]);

        while (queue.length > 0) {
            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;
                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;
    }

    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) {
                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(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(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;
    }

    dump() {
        return [...this.nodes.values()];
    }

    _uuid() {
        if (typeof crypto !== 'undefined' && crypto.randomUUID) return 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);
        });
    }
}

// ── activation.js ─────────────────────────────────────────────────────────────

export const PRUNE_THRESHOLD = 0.01;

export function spreadActivation(graph, seeds, {
    maxDepth = 3,
    limit = 20,
    pruneThreshold = PRUNE_THRESHOLD,
} = {}) {
    const bestStrength = new Map();
    const queue = [];
    const seedSet = new Set(seeds);

    for (const seed of seeds) {
        if (!graph.get(seed)) continue;
        queue.push({ id: seed, strength: 1.0, hops: 0 });
        bestStrength.set(seed, { strength: 1.0, hops: 0 });
    }

    while (queue.length > 0) {
        let bestIdx = 0;
        for (let i = 1; i < queue.length; i++) {
            if (queue[i].strength > queue[bestIdx].strength) bestIdx = i;
        }
        const { id, strength, hops } = queue.splice(bestIdx, 1)[0];
        if (hops >= maxDepth) continue;
        const node = graph.get(id);
        if (!node) continue;
        for (const edgeId of node.edges) {
            const edge = graph.edges.get(edgeId);
            if (!edge) continue;
            const target = graph.get(edge.to);
            if (!target) continue;
            const targetStrength = strength * edge.weight * Math.max(0, target.importance);
            if (targetStrength <= pruneThreshold) continue;
            const prev = bestStrength.get(edge.to);
            if (!prev || targetStrength > prev.strength) {
                const nextHops = hops + 1;
                bestStrength.set(edge.to, { strength: targetStrength, hops: nextHops });
                queue.push({ id: edge.to, strength: targetStrength, hops: nextHops });
            }
        }
    }

    const results = [];
    for (const [nodeId, { strength, hops }] of bestStrength) {
        if (seedSet.has(nodeId)) continue;
        const node = graph.get(nodeId);
        if (node) results.push({ nodeId, strength, hops, node });
    }
    results.sort((a, b) => b.strength - a.strength);
    return results.slice(0, limit);
}

export function activationStrength(graph, fromId, toId, maxDepth = 3) {
    const results = spreadActivation(graph, [fromId], { maxDepth });
    const found = results.find(r => r.nodeId === toId);
    return found ? found.strength : 0;
}

export function reachableNodes(graph, seedId, maxDepth = 3) {
    return graph.activate(seedId, maxDepth);
}

// ── renderer.js ───────────────────────────────────────────────────────────────

export class Renderer {
    constructor(root, component) {
        this.root = root;
        this.component = component;
        this._currentHtml = '';
        this._boundHandlers = new WeakMap();
    }

    mount() {
        this._currentHtml = this.component.render();
        this.root.innerHTML = this._currentHtml;
        this._bindEvents();
        if (typeof this.component.onMount === 'function') {
            this.component.onMount();
        }
    }

    patch(_activatedNodes) {
        const newHtml = this.component.render();
        if (newHtml !== this._currentHtml) {
            const focused = document.activeElement;
            const focusedId = focused && focused !== document.body ? focused.id : null;
            this.root.innerHTML = newHtml;
            this._bindEvents();
            this._currentHtml = newHtml;
            if (focusedId) {
                const el = this.root.querySelector(`#${CSS.escape(focusedId)}`);
                if (el) el.focus();
            }
        }
    }

    _bindEvents() {
        const comp = this.component;
        const eventNames = [
            'click', 'input', 'change', 'mouseenter', 'mouseleave',
            'keydown', 'keyup', 'keypress', 'focus', 'blur',
            'submit', 'mousedown', 'mouseup', 'dblclick', 'contextmenu',
        ];
        for (const eventName of eventNames) {
            const attr = `data-el-${eventName}`;
            this.root.querySelectorAll(`[${attr}]`).forEach(el => {
                const handlerExpr = el.getAttribute(attr);
                if (!handlerExpr) return;
                const handler = this._compileHandler(handlerExpr, comp);
                if (handler) {
                    const prev = this._boundHandlers.get(el);
                    if (prev && prev[eventName]) el.removeEventListener(eventName, prev[eventName]);
                    el.addEventListener(eventName, handler);
                    if (!this._boundHandlers.has(el)) this._boundHandlers.set(el, {});
                    this._boundHandlers.get(el)[eventName] = handler;
                }
            });
        }
    }

    _compileHandler(expr, comp) {
        try {
            const decoded = expr
                .replace(/&quot;/g, '"')
                .replace(/&#39;/g, "'")
                .replace(/&amp;/g, '&')
                .replace(/&lt;/g, '<')
                .replace(/&gt;/g, '>');
            if (decoded.includes('=>')) {
                // eslint-disable-next-line no-new-func
                const fn = new Function('__self', `return ${decoded}`)(comp);
                return typeof fn === 'function' ? fn : null;
            } else if (decoded.includes('(')) {
                // eslint-disable-next-line no-new-func
                return new Function('__self', 'event', `${decoded}`).bind(null, comp);
            } else {
                return comp[decoded] ? comp[decoded].bind(comp) : null;
            }
        } catch (e) {
            console.warn(`el-ui: failed to compile handler: ${expr}`, e);
            return null;
        }
    }
}

// ── router.js ─────────────────────────────────────────────────────────────────

export class Router {
    constructor(graph, routes) {
        this.graph = graph;
        this.routes = routes;
        this.currentPath = window.location.pathname;
        this._routeNodes = {};
        this._subscribers = [];

        for (const [path] of Object.entries(routes)) {
            const id = graph.seed({ type: 'route', name: path, content: path, importance: 0.3 });
            this._routeNodes[path] = id;
        }

        const paths = Object.keys(routes).sort();
        for (let i = 0; i < paths.length - 1; i++) {
            const parent = paths[i];
            const child = paths[i + 1];
            if (child.startsWith(parent) && parent !== child) {
                graph.connect(this._routeNodes[parent], this._routeNodes[child], {
                    weight: 0.8,
                    relation: 'subroute',
                });
            }
        }

        window.addEventListener('popstate', () => this._activate(window.location.pathname));
    }

    navigate(path, replace = false) {
        if (path === this.currentPath) return;
        if (replace) history.replaceState({}, '', path);
        else history.pushState({}, '', path);
        this._activate(path);
    }

    currentComponent() {
        return this.routes[this.currentPath] ?? this.routes['*'] ?? null;
    }

    subscribe(callback) {
        this._subscribers.push(callback);
        return () => {
            const idx = this._subscribers.indexOf(callback);
            if (idx >= 0) this._subscribers.splice(idx, 1);
        };
    }

    href(path) { return path; }

    _activate(path) {
        const prevPath = this.currentPath;
        this.currentPath = path;
        const matchedPath = this._matchPath(path);
        if (matchedPath && this._routeNodes[matchedPath]) {
            this.graph.activate(this._routeNodes[matchedPath]);
            const node = this.graph.get(this._routeNodes[matchedPath]);
            if (node) node.importance = Math.min(1.0, node.importance + 0.2);
        }
        if (path !== prevPath) this._subscribers.forEach(cb => cb(path));
    }

    _matchPath(path) {
        if (this.routes[path]) return path;
        const candidates = Object.keys(this.routes)
            .filter(r => r !== '*' && path.startsWith(r))
            .sort((a, b) => b.length - a.length);
        if (candidates.length > 0) return candidates[0];
        return '*';
    }
}

// ── Component base class & mount() ───────────────────────────────────────────

export class Component {
    constructor(props = {}) {
        this._graph = new Graph();
        this._stateNodes = {};
        this._state = {};
        this.props = props;
        this._renderer = null;
    }

    setState(name, value) {
        if (this._stateNodes[name] !== undefined) {
            this._graph.update(this._stateNodes[name], value);
            if (this._renderer) this._renderer.patch();
        }
    }

    _child(ComponentClass, props = {}) {
        try {
            const instance = new ComponentClass(props);
            instance._graph = this._graph;
            return instance.render();
        } catch (e) {
            console.warn(`el-ui: failed to render child ${ComponentClass?.name}`, e);
            return `<!-- el-ui: render error in ${ComponentClass?.name} -->`;
        }
    }

    render() { return ''; }
    onMount() {}
}

export function mount(ComponentClass, selector, props = {}) {
    const root = document.querySelector(selector);
    if (!root) throw new Error(`el-ui: no element found for selector '${selector}'`);
    const component = new ComponentClass(props);
    const renderer = new Renderer(root, component);
    component._renderer = renderer;
    renderer.mount();
    return component;
}

export default { mount, Component, Graph, Renderer, Router };
