// yaml-complete.js — deterministic, schema-driven completion + hover docs for
// the browse YAML editors (markdown front matter + .zddc). NO heuristics, no
// AI: every candidate and doc string comes from a PROVIDER backed by the
// converter's field list or the .zddc JSON Schema.
//
// A provider answers three questions about a position, identified by its key
// PATH (the array of parent keys):
//   keysAt(path)         → [{name, hint, values}]  valid child keys here
//   valuesFor(path, key) → [string] | null         enum/boolean values
//   describe(path, key)  → string | null           doc text (for hover)
// The CodeMirror plumbing (indent→path, sibling scan, show-hint, hover) is
// shared; only the provider differs between the flat front matter and the
// nested .zddc schema. Requires CodeMirror 5 + the show-hint add-on.
(function () {
    'use strict';
    if (!window.app) window.app = {};
    if (!window.app.modules) window.app.modules = {};

    function indentOf(line) { var m = line.match(/^(\s*)/); return m ? m[1].length : 0; }
    function isBlankOrComment(line) { return /^\s*$/.test(line) || /^\s*#/.test(line); }
    function truncate(s, n) { s = String(s); return s.length > n ? s.slice(0, n - 1) + '…' : s; }

    // Parent key-path for a line, derived from YAML indentation: walk upward
    // collecting each "key:" line at a strictly smaller indent.
    function pathAt(cm, lineNo) {
        var path = [];
        var target = indentOf(cm.getLine(lineNo));
        for (var ln = lineNo - 1; ln >= 0 && target > 0; ln--) {
            var line = cm.getLine(ln);
            if (isBlankOrComment(line)) continue;
            var ind = indentOf(line);
            if (ind < target) {
                var m = line.match(/^\s*([\w.\-]+)\s*:/);
                if (m) { path.unshift(m[1]); target = ind; }
            }
        }
        return path;
    }

    // Sibling keys already present at the same indent within this block, so we
    // don't re-suggest a key the author already wrote.
    function presentSiblings(cm, lineNo, indent) {
        var present = {};
        [-1, 1].forEach(function (dir) {
            for (var ln = lineNo + dir; ln >= 0 && ln < cm.lineCount(); ln += dir) {
                var line = cm.getLine(ln);
                if (isBlankOrComment(line)) continue;
                var ind = indentOf(line);
                if (ind < indent) break;          // left the block
                if (ind === indent) {
                    var m = line.match(/^\s*([\w.\-]+)\s*:/);
                    if (m) present[m[1]] = true;
                }
            }
        });
        return present;
    }

    function keyItem(k, hinter) {
        var item = {
            text: k.name + ': ',
            displayText: k.name + (k.hint ? '  — ' + truncate(k.hint, 64) : '')
        };
        // An enum key inserts "key: " then immediately opens its value menu.
        if (k.values && k.values.length) {
            item.hint = function (cmi, data, comp) {
                cmi.replaceRange(comp.text, data.from, data.to);
                setTimeout(function () { cmi.showHint({ hint: hinter, completeSingle: false }); }, 0);
            };
        }
        return item;
    }

    function makeHinter(provider) {
        function hinter(cm) {
            var CM = window.CodeMirror;
            if (!CM) return null;
            var cur = cm.getCursor();
            var before = cm.getLine(cur.line).slice(0, cur.ch);
            var colon = before.indexOf(':');
            var path = pathAt(cm, cur.line);

            if (colon === -1) {
                // KEY context.
                var m = before.match(/^(\s*)([\w.\-]*)$/);
                if (!m) return null;
                var indent = m[1], typed = m[2];
                var keys = provider.keysAt(path) || [];
                if (!keys.length) return null;
                var present = presentSiblings(cm, cur.line, indent.length);
                var list = [];
                keys.forEach(function (k) {
                    if (present[k.name]) return;
                    if (typed && k.name.indexOf(typed) !== 0) return;
                    list.push(keyItem(k, hinter));
                });
                if (!list.length) return null;
                return { list: list, from: CM.Pos(cur.line, indent.length), to: cur };
            }

            // VALUE context.
            var key = before.slice(0, colon).trim();
            var values = provider.valuesFor(path, key) || [];
            if (!values.length) return null;
            var rest = before.slice(colon + 1);
            var valTyped = rest.replace(/^\s*/, '');
            var valStart = colon + 1 + (rest.length - valTyped.length);
            var vlist = [];
            values.forEach(function (v) {
                if (valTyped && v.indexOf(valTyped) !== 0) return;
                vlist.push({ text: v, displayText: v });
            });
            if (!vlist.length) return null;
            return { list: vlist, from: CM.Pos(cur.line, valStart), to: cur };
        }
        return hinter;
    }

    // Lightweight hover docs: hover a "key:" → its schema description. No
    // add-on — a debounced mousemove over the editor + a fixed-position tip.
    function attachHover(cm, provider) {
        var tip = null, timer = null;
        function hide() { if (tip && tip.parentNode) tip.parentNode.removeChild(tip); tip = null; }
        function show(text, x, y) {
            hide();
            tip = document.createElement('div');
            tip.className = 'cm-doc-tip';
            tip.textContent = text;
            document.body.appendChild(tip);
            tip.style.left = x + 'px';
            tip.style.top = (y + 16) + 'px';
        }
        var wrap = cm.getWrapperElement();
        wrap.addEventListener('mousemove', function (e) {
            if (timer) clearTimeout(timer);
            var ex = e.clientX, ey = e.clientY;
            timer = setTimeout(function () {
                if (!wrap.isConnected) { hide(); return; }
                try {
                    var pos = cm.coordsChar({ left: ex, top: ey }, 'window');
                    var line = cm.getLine(pos.line) || '';
                    var m = line.match(/^\s*([\w.\-]+)\s*:/);
                    if (!m) { hide(); return; }
                    var keyStart = line.indexOf(m[1]);
                    if (pos.ch < keyStart || pos.ch > keyStart + m[1].length) { hide(); return; }
                    var doc = provider.describe(pathAt(cm, pos.line), m[1]);
                    if (doc) show(doc, ex, ey); else hide();
                } catch (_e) { hide(); }
            }, 350);
        });
        wrap.addEventListener('mouseleave', function () { if (timer) clearTimeout(timer); hide(); });
        cm.on('cursorActivity', hide);
        cm.on('changes', hide);
    }

    // Wire completion (Ctrl-Space + auto-trigger as you type) and hover docs
    // onto a CodeMirror instance. opts.readOnly skips the typing trigger;
    // opts.hover:false skips hover.
    function attach(cm, provider, opts) {
        opts = opts || {};
        var hinter = makeHinter(provider);
        var keys = Object.assign({}, cm.getOption('extraKeys') || {}, {
            'Ctrl-Space': function (c) { c.showHint({ hint: hinter, completeSingle: false }); }
        });
        cm.setOption('extraKeys', keys);
        if (!opts.readOnly) {
            cm.on('inputRead', function (c, change) {
                if (!change.text || change.text.length !== 1) return; // skip paste/delete
                if (!/[\w.\-]/.test(change.text[0])) return;
                c.showHint({ hint: hinter, completeSingle: false });
            });
        }
        if (opts.hover !== false) attachHover(cm, provider);
        return hinter;
    }

    // ── Providers ───────────────────────────────────────────────────────────

    // Flat: a fixed field list [{name, hint, values}] at the root, nothing
    // nested (front matter). opts.exclude = names never suggested.
    function flatProvider(getFields, opts) {
        opts = opts || {};
        var exclude = {};
        (opts.exclude || []).forEach(function (n) { exclude[n] = true; });
        function fields() { return getFields() || []; }
        function find(name) {
            var fs = fields();
            for (var i = 0; i < fs.length; i++) if (fs[i].name === name) return fs[i];
            return null;
        }
        return {
            keysAt: function (path) {
                if (path.length) return [];
                return fields().filter(function (f) { return !exclude[f.name]; })
                    .map(function (f) { return { name: f.name, hint: f.hint, values: f.values }; });
            },
            valuesFor: function (path, key) {
                if (path.length) return null;
                var f = find(key); return f ? f.values : null;
            },
            describe: function (path, key) {
                if (path.length) return null;
                var f = find(key); return f ? f.hint : null;
            }
        };
    }

    // Schema: a JSON Schema (draft-2020-12 subset). Resolves nested key-paths
    // through properties / additionalProperties / patternProperties and the
    // recursive $ref:"#" .zddc uses for paths:. Keys = object property names;
    // values = enum / boolean.
    function schemaProvider(getSchema) {
        function root() { return getSchema(); }
        function deref(node) { return (node && node.$ref === '#') ? root() : node; }
        function stepInto(node, seg) {
            node = deref(node);
            if (!node || node.type !== 'object') return null;
            if (node.properties && node.properties[seg]) return node.properties[seg];
            if (node.additionalProperties && typeof node.additionalProperties === 'object') {
                return node.additionalProperties;
            }
            if (node.patternProperties) {
                for (var p in node.patternProperties) {
                    if (Object.prototype.hasOwnProperty.call(node.patternProperties, p)) {
                        return node.patternProperties[p];
                    }
                }
            }
            return null;
        }
        function containerAt(path) {
            var node = deref(root());
            for (var i = 0; i < path.length; i++) {
                node = stepInto(node, path[i]);
                if (!node) return null;
                node = deref(node);
            }
            return node;
        }
        function valuesOf(node) {
            node = deref(node);
            if (!node) return null;
            if (Array.isArray(node.enum)) return node.enum.map(String);
            if (node.type === 'boolean') return ['true', 'false'];
            return null;
        }
        function keyNodeAt(path, key) {
            var c = containerAt(path);
            if (!c || !c.properties) return null;
            return c.properties[key] || null;
        }
        return {
            keysAt: function (path) {
                var c = containerAt(path);
                if (!c || c.type !== 'object' || !c.properties) return [];
                return Object.keys(c.properties).map(function (name) {
                    var n = deref(c.properties[name]) || {};
                    return { name: name, hint: n.description, values: valuesOf(n) };
                });
            },
            valuesFor: function (path, key) { return valuesOf(keyNodeAt(path, key)); },
            describe: function (path, key) {
                var n = deref(keyNodeAt(path, key));
                return n ? n.description : null;
            }
        };
    }

    window.app.modules.yamlComplete = {
        attach: attach,
        makeHinter: makeHinter,
        flatProvider: flatProvider,
        schemaProvider: schemaProvider
    };
})();
