<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>config - go2rtc</title>
    <style>
        html, body {
            height: 100%;
        }

        #config {
            flex: 1 1 auto;
            border-top: 1px solid #ccc;
            min-height: 300px;
        }
    </style>
</head>
<body>

<script src="main.js"></script>

<main>
    <div>
        <button id="save">Save & Restart</button>
        <button id="suggest" title="ctrl + space">Suggest</button>
    </div>
</main>
<div id="config"></div>

<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs/loader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
<script>
    /* global require, monaco */
    const monacoRoot = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min';

    window.MonacoEnvironment = {
        getWorkerUrl: function () {
            return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
                self.MonacoEnvironment = { baseUrl: '${monacoRoot}/' };
                importScripts('${monacoRoot}/vs/base/worker/workerMain.js');
            `)}`;
        }
    };

    require.config({paths: {vs: `${monacoRoot}/vs`}});

    require(['vs/editor/editor.main'], () => {
        const container = document.getElementById('config');
        container.textContent = '';

        const ensureYamlLanguage = () => {
            const languages =
                (window.monaco &&
                    monaco.languages &&
                    typeof monaco.languages.getLanguages === 'function' &&
                    monaco.languages.getLanguages()) ||
                [];
            const hasYaml = languages.some((l) => l.id === 'yaml');
            if (hasYaml) return;

            monaco.languages.register({
                id: 'yaml',
                extensions: ['.yaml', '.yml'],
                aliases: ['YAML', 'yaml'],
                mimetypes: ['application/x-yaml', 'text/yaml'],
            });

            monaco.languages.setLanguageConfiguration('yaml', {
                comments: {lineComment: '#'},
                brackets: [['{', '}'], ['[', ']'], ['(', ')']],
                autoClosingPairs: [
                    {open: '{', close: '}'},
                    {open: '[', close: ']'},
                    {open: '(', close: ')'},
                    {open: '"', close: '"'},
                    {open: '\'', close: '\''},
                ],
                surroundingPairs: [
                    {open: '{', close: '}'},
                    {open: '[', close: ']'},
                    {open: '(', close: ')'},
                    {open: '"', close: '"'},
                    {open: '\'', close: '\''},
                ],
            });

            monaco.languages.setMonarchTokensProvider('yaml', {
                tokenizer: {
                    root: [
                        [/^\s*(---|\.\.\.)\s*$/, 'delimiter'],
                        [/#.*$/, 'comment'],
                        [/^\s*-\s+/, 'delimiter'],
                        [/[A-Za-z0-9_-]+(?=\s*:)/, 'key'],
                        [/:/, 'delimiter'],
                        [/[{}\[\](),]/, 'delimiter'],
                        [/\b(true|false|null|~)\b/, 'keyword'],
                        [/-?\d+(\.\d+)?\b/, 'number'],
                        [/"/, 'string', '@string_double'],
                        [/'/, 'string', '@string_single'],
                        [/[^#\s{}\[\](),]+/, 'string'],
                        [/\s+/, ''],
                    ],
                    string_double: [
                        [/[^\\"]+/, 'string'],
                        [/\\./, 'string.escape'],
                        [/"/, 'string', '@pop'],
                    ],
                    string_single: [
                        [/[^']+/, 'string'],
                        [/'/, 'string', '@pop'],
                    ],
                },
            });
        };

        ensureYamlLanguage();

        const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        monaco.editor.setTheme(prefersDark ? 'vs-dark' : 'vs');

        const editor = monaco.editor.create(container, {
            language: 'yaml',
            minimap: {enabled: false},
            automaticLayout: true,
            tabSize: 2,
            insertSpaces: true,
            quickSuggestions: {other: true, comments: false, strings: true},
            suggestOnTriggerCharacters: true,
            wordBasedSuggestions: false,
            suggest: {showWords: false},
            scrollBeyondLastLine: false,
        });

        const stripInlineComment = (line) => {
            let inSingle = false;
            let inDouble = false;
            for (let i = 0; i < line.length; i++) {
                const ch = line[i];
                if (ch === '\'' && !inDouble) {
                    inSingle = !inSingle;
                    continue;
                }
                if (ch === '"' && !inSingle) {
                    inDouble = !inDouble;
                    continue;
                }
                if (ch === '#' && !inSingle && !inDouble) {
                    if (i === 0 || /\s/.test(line[i - 1])) return line.slice(0, i);
                }
            }
            return line;
        };

        const countIndent = (line) => {
            let indent = 0;
            for (let i = 0; i < line.length; i++) {
                if (line[i] === ' ') {
                    indent++;
                } else if (line[i] === '\t') {
                    indent += 2;
                } else {
                    break;
                }
            }
            return indent;
        };

        const parseListItem = (line) => {
            const m = line.match(/^([ \t]*)-/);
            if (!m) return null;

            const dashIndex = m[1].length;
            if (dashIndex + 1 < line.length && !/\s/.test(line[dashIndex + 1])) return null;

            let afterDashIndex = dashIndex + 1;
            while (afterDashIndex < line.length && /\s/.test(line[afterDashIndex])) afterDashIndex++;

            const spacesAfterDash = Math.max(1, afterDashIndex - (dashIndex + 1));

            return {
                indent: countIndent(m[1]),
                dashIndex,
                afterDashIndex,
                rest: line.slice(afterDashIndex),
                contentIndent: countIndent(m[1]) + 1 + spacesAfterDash,
            };
        };

        const parseKey = (line) => {
            const m = line.match(/^([ \t]*)/);
            const indentStr = m ? m[0] : '';
            const indentIndex = indentStr.length;
            const indent = countIndent(indentStr);

            if (indentIndex >= line.length) return null;

            const i = indentIndex;
            let key = '';
            let rawKey = '';
            let isQuoted = false;
            let keyStartIndex = i;
            let keyEndIndex = i;
            let colonIndex = -1;

            const parseQuotedKey = (quoteChar) => {
                isQuoted = true;
                let j = i + 1;
                if (quoteChar === '"') {
                    while (j < line.length) {
                        if (line[j] === '\\') {
                            j += 2;
                            continue;
                        }
                        if (line[j] === '"') break;
                        j++;
                    }
                } else {
                    while (j < line.length) {
                        if (line[j] === '\'') {
                            if (line[j + 1] === '\'') {
                                j += 2;
                                continue;
                            }
                            break;
                        }
                        j++;
                    }
                }

                if (j >= line.length) return null;
                rawKey = line.slice(i, j + 1);
                if (quoteChar === '"') {
                    key = line.slice(i + 1, j).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
                } else {
                    key = line.slice(i + 1, j).replace(/''/g, '\'');
                }
                keyStartIndex = i;
                keyEndIndex = j + 1;

                let k = j + 1;
                while (k < line.length && /\s/.test(line[k])) k++;
                if (k >= line.length || line[k] !== ':') return null;
                colonIndex = k;
                return colonIndex;
            };

            if (line[i] === '"' || line[i] === '\'') {
                if (parseQuotedKey(line[i]) === null) return null;
            } else {
                let j = i;
                while (j < line.length) {
                    if (line[j] === ':') {
                        if (j + 1 >= line.length || /\s/.test(line[j + 1])) {
                            colonIndex = j;
                            break;
                        }
                    }
                    j++;
                }
                if (colonIndex === -1) return null;
                rawKey = line.slice(i, colonIndex).replace(/\s+$/, '');
                if (!rawKey) return null;
                key = rawKey;
                keyStartIndex = i;
                keyEndIndex = i + rawKey.length;
            }

            const after = line.slice(colonIndex + 1);
            const isContainer = after.trim() === '' || after.trim().startsWith('#');
            const valueStartIndex = colonIndex + 1;

            return {
                indent,
                key,
                rawKey,
                isQuoted,
                isContainer,
                after,
                keyStartIndex,
                keyEndIndex,
                colonIndex,
                valueStartIndex
            };
        };

        const unique = (arr) => [...new Set(arr)];

        const toYamlScalar = (v) => {
            if (v === '') return '\'\'';
            if (typeof v === 'string') return v;
            if (typeof v === 'number') return String(v);
            if (typeof v === 'boolean') return v ? 'true' : 'false';
            return JSON.stringify(v);
        };

        const createSchemaTools = (schemaRoot) => {
            const resolveRef = (schema, seen = new Set()) => {
                if (!schema || typeof schema !== 'object') return schema;
                if (typeof schema.$ref === 'string') {
                    const ref = schema.$ref;
                    if (ref.startsWith('#/definitions/')) {
                        if (seen.has(ref)) return schema;
                        seen.add(ref);
                        const name = ref.slice('#/definitions/'.length);
                        const def = schemaRoot.definitions && schemaRoot.definitions[name];
                        if (!def) return schema;
                        const resolved = resolveRef(def, seen);
                        const rest = Object.assign({}, schema);
                        delete rest.$ref;
                        return Object.assign({}, resolved, rest);
                    }
                }
                return schema;
            };

            const mergeProps = (schemas) => {
                const props = {};
                for (const s of schemas) {
                    const schema = resolveRef(s);
                    if (schema && schema.properties && typeof schema.properties === 'object') {
                        Object.assign(props, schema.properties);
                    }
                }
                return props;
            };

            const getObjectProperties = (schema) => {
                schema = resolveRef(schema);
                if (!schema) return {};
                if (schema.properties && typeof schema.properties === 'object') return schema.properties;
                if (Array.isArray(schema.anyOf)) return mergeProps(schema.anyOf);
                return {};
            };

            const getPropertySchema = (schema, key) => {
                schema = resolveRef(schema);
                if (!schema) return null;

                if (schema.properties && schema.properties[key]) return resolveRef(schema.properties[key]);

                if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
                    return resolveRef(schema.additionalProperties);
                }

                if (Array.isArray(schema.anyOf)) {
                    for (const alt of schema.anyOf) {
                        const res = getPropertySchema(alt, key);
                        if (res) return res;
                    }
                }

                return null;
            };

            const getValueSuggestions = (schema) => {
                schema = resolveRef(schema);
                if (!schema) return [];

                const values = [];

                const addFrom = (s) => {
                    s = resolveRef(s);
                    if (!s) return;
                    if (Array.isArray(s.enum)) values.push(...s.enum);
                    if ('const' in s) values.push(s.const);
                    if (Array.isArray(s.examples)) values.push(...s.examples);
                    if ('default' in s) values.push(s.default);
                };

                if (Array.isArray(schema.anyOf)) {
                    for (const alt of schema.anyOf) addFrom(alt);
                } else {
                    addFrom(schema);
                }

                return unique(values);
            };

            const getSchemaTypes = (schema, seen = new Set()) => {
                schema = resolveRef(schema);
                if (!schema || typeof schema !== 'object') return new Set();

                if (Array.isArray(schema.anyOf)) {
                    const types = new Set();
                    for (const alt of schema.anyOf) {
                        for (const t of getSchemaTypes(alt, seen)) types.add(t);
                    }
                    return types;
                }

                if (Array.isArray(schema.oneOf)) {
                    const types = new Set();
                    for (const alt of schema.oneOf) {
                        for (const t of getSchemaTypes(alt, seen)) types.add(t);
                    }
                    return types;
                }

                if (Array.isArray(schema.type)) return new Set(schema.type);
                if (typeof schema.type === 'string') return new Set([schema.type]);
                if (schema.properties || schema.additionalProperties) return new Set(['object']);
                if (schema.items) return new Set(['array']);
                return new Set();
            };

            const schemaAllowsType = (schema, actualType) => {
                const types = getSchemaTypes(schema);
                if (actualType === 'integer' && types.has('number')) return true;
                return types.has(actualType);
            };

            const schemaTypesLabel = (schema) => {
                const types = Array.from(getSchemaTypes(schema));
                if (types.length === 0) return 'any';
                return types.sort().join(' | ');
            };

            const getArrayItemSchema = (schema) => {
                schema = resolveRef(schema);
                if (!schema) return null;
                if (schema.type === 'array' && schema.items) return resolveRef(schema.items);
                if (Array.isArray(schema.anyOf)) {
                    for (const alt of schema.anyOf) {
                        const item = getArrayItemSchema(alt);
                        if (item) return item;
                    }
                }
                return null;
            };

            return {
                schemaRoot,
                resolveRef,
                getObjectProperties,
                getPropertySchema,
                getValueSuggestions,
                getSchemaTypes,
                schemaAllowsType,
                schemaTypesLabel,
                getArrayItemSchema,
            };
        };

        const isIntLike = (s) => /^[+-]?\d+$/.test(s);
        const isNumberLike = (s) => (
            /^[+-]?(?:\d*\.\d+|\d+\.\d*)(?:[eE][+-]?\d+)?$/.test(s) ||
            /^[+-]?\d+(?:[eE][+-]?\d+)$/.test(s) ||
            isIntLike(s)
        );

        const classifyYamlScalar = (raw) => {
            const v = raw.trim();
            if (!v) return {type: 'null'};
            if (/^\$\{[^}{]+\}$/.test(v)) return {type: 'dynamic'};
            if (v.startsWith('[')) return {type: 'array'};
            if (v.startsWith('{')) return {type: 'object'};
            if (v.startsWith('"') || v.startsWith('\'')) return {type: 'string'};
            if (v === 'true' || v === 'false') return {type: 'boolean'};
            if (v === 'null' || v === '~') return {type: 'null'};
            if (isIntLike(v)) return {type: 'integer'};
            if (isNumberLike(v)) return {type: 'number'};
            return {type: 'string'};
        };

        const parseYamlValue = (raw) => {
            const trimmed = raw.trim();
            if (!trimmed) return {ok: false};
            if (/^\$\{[^}{]+\}$/.test(trimmed)) return {ok: false, dynamic: true};
            if (trimmed.startsWith('|') || trimmed.startsWith('>')) return {ok: false, block: true};
            if (window.jsyaml && window.jsyaml.load) {
                try {
                    return {ok: true, value: window.jsyaml.load(trimmed)};
                } catch (e) {
                    // nothing
                }
            }
            if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
                const inner = trimmed.slice(1, -1);
                return {ok: true, value: inner.replace(/\\"/g, '"').replace(/\\\\/g, '\\')};
            }
            if (trimmed.startsWith('\'') && trimmed.endsWith('\'') && trimmed.length >= 2) {
                const inner = trimmed.slice(1, -1);
                return {ok: true, value: inner.replace(/''/g, '\'')};
            }
            if (trimmed === 'true' || trimmed === 'false') return {ok: true, value: trimmed === 'true'};
            if (trimmed === 'null' || trimmed === '~') return {ok: true, value: null};
            if (isIntLike(trimmed)) return {ok: true, value: parseInt(trimmed, 10)};
            if (isNumberLike(trimmed)) return {ok: true, value: Number(trimmed)};
            return {ok: true, value: trimmed};
        };

        const lintYamlModel = (model, schemaTools) => {
            const markers = [];
            const markedLines = new Set();
            let blockScalarParentIndent = null;

            const isBlockScalarHeader = (text) => {
                const rawText = (text == null) ? '' : text;
                const t = rawText.trimStart ? rawText.trimStart() : rawText.replace(/^\s+/, '');
                return t.startsWith('|') || t.startsWith('>');
            };

            const checkChildIndent = (ctx, childIndent, lineNumber) => {
                if (!ctx) return;
                if (ctx.childIndent == null) {
                    ctx.childIndent = childIndent;
                    return;
                }
                if (childIndent !== ctx.childIndent) {
                    markLineError(lineNumber, `YAML: inconsistent indentation (expected ${ctx.childIndent} spaces)`, 1);
                }
            };

            const markLineError = (lineNumber, message, startColumn = 1) => {
                if (markedLines.has(`${lineNumber}:${message}`)) return;
                markedLines.add(`${lineNumber}:${message}`);
                const lineText = model.getLineContent(lineNumber);
                markers.push({
                    severity: monaco.MarkerSeverity.Error,
                    message,
                    startLineNumber: lineNumber,
                    startColumn,
                    endLineNumber: lineNumber,
                    endColumn: Math.max(startColumn + 1, lineText.length + 1),
                });
            };

            if (window.jsyaml && window.jsyaml.load) {
                try {
                    window.jsyaml.load(model.getValue());
                } catch (e) {
                    const mark = (e && e.mark) || {};
                    const line = typeof mark.line === 'number' ? mark.line + 1 : 1;
                    const column = typeof mark.column === 'number' ? mark.column + 1 : 1;
                    const reason = e && e.reason;
                    const messageText = e && e.message;
                    markLineError(line, reason ? `YAML: ${reason}` : `YAML: ${messageText || 'Invalid YAML'}`, column);
                }
            }

            const pushMarker = (m) => markers.push(m);

            const getExpectedContainerType = (schema) => {
                if (!schemaTools) return null;
                const types = schemaTools.getSchemaTypes(schema);
                const wantsObject = types.has('object');
                const wantsArray = types.has('array');
                if (wantsObject && !wantsArray) return 'object';
                if (wantsArray && !wantsObject) return 'array';
                return null;
            };

            const stack = [{
                indent: -1,
                schema: (schemaTools && schemaTools.schemaRoot) || null,
                expected: 'object',
                actual: null,
                keys: new Map(),
                childIndent: null,
                origin: null,
                reportedTypeMismatch: false,
            }];

            let hasTopLevelKey = false;
            let hasTopLevelList = false;
            const lineCount = model.getLineCount();
            for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
                let line = model.getLineContent(lineNumber);
                if (!line.trim()) continue;
                line = stripInlineComment(line).trimEnd();
                if (!line.trim()) continue;
                const kv = parseKey(line);
                if (kv && kv.indent === 0) hasTopLevelKey = true;
                const li = parseListItem(line);
                if (li && li.indent === 0) hasTopLevelList = true;
            }

            const setActualType = (ctx, actual, fallbackLineNumber) => {
                if (ctx.actual !== null) return;
                ctx.actual = actual;
                if (ctx.origin && ctx.expected && ctx.expected !== actual && !ctx.reportedTypeMismatch) {
                    pushMarker({
                        severity: monaco.MarkerSeverity.Error,
                        message: `Type mismatch: expected ${ctx.expected}, got ${actual}`,
                        startLineNumber: ctx.origin.lineNumber,
                        startColumn: ctx.origin.startColumn,
                        endLineNumber: ctx.origin.lineNumber,
                        endColumn: ctx.origin.endColumn,
                    });
                    ctx.reportedTypeMismatch = true;
                } else if (!ctx.origin && ctx.expected && ctx.expected !== actual && fallbackLineNumber) {
                    pushMarker({
                        severity: monaco.MarkerSeverity.Error,
                        message: `Type mismatch: expected ${ctx.expected}, got ${actual}`,
                        startLineNumber: fallbackLineNumber,
                        startColumn: 1,
                        endLineNumber: fallbackLineNumber,
                        endColumn: 2,
                    });
                    ctx.reportedTypeMismatch = true;
                }
            };

            const checkValueType = (schema, actualType, lineNumber, startColumn, endColumn, keyName) => {
                if (!schemaTools || !schema) return;
                if (actualType === 'dynamic') return;
                if (schemaTools.schemaAllowsType(schema, actualType)) return;
                pushMarker({
                    severity: monaco.MarkerSeverity.Error,
                    message: `Type mismatch for ${keyName ? `"${keyName}"` : 'value'}: expected ${schemaTools.schemaTypesLabel(schema)}, got ${actualType}`,
                    startLineNumber: lineNumber,
                    startColumn,
                    endLineNumber: lineNumber,
                    endColumn: Math.max(startColumn + 1, endColumn),
                });
            };

            const valueEquals = (a, b) => {
                if (a === b) return true;
                if (typeof a !== typeof b) return false;
                if (a && b && typeof a === 'object') {
                    const aIsArray = Array.isArray(a);
                    const bIsArray = Array.isArray(b);
                    if (aIsArray !== bIsArray) return false;
                    if (aIsArray) {
                        if (a.length !== b.length) return false;
                        for (let i = 0; i < a.length; i++) {
                            if (!valueEquals(a[i], b[i])) return false;
                        }
                        return true;
                    }
                    const aKeys = Object.keys(a);
                    const bKeys = Object.keys(b);
                    if (aKeys.length !== bKeys.length) return false;
                    for (const key of aKeys) {
                        if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
                        if (!valueEquals(a[key], b[key])) return false;
                    }
                    return true;
                }
                return false;
            };

            const schemaAllowsTypeLoose = (schema, actualType) => {
                if (!schemaTools || !schema) return true;
                const types = schemaTools.getSchemaTypes(schema);
                if (types.size === 0) return true;
                if (actualType === 'integer' && types.has('number')) return true;
                return types.has(actualType);
            };

            const collectConstraintSchemas = (schema, actualType) => {
                if (!schemaTools || !schema) return [];
                schema = schemaTools.resolveRef(schema);
                if (!schema) return [];
                if (Array.isArray(schema.anyOf)) {
                    const res = [];
                    for (const alt of schema.anyOf) res.push(...collectConstraintSchemas(alt, actualType));
                    return res;
                }
                if (Array.isArray(schema.oneOf)) {
                    const res = [];
                    for (const alt of schema.oneOf) res.push(...collectConstraintSchemas(alt, actualType));
                    return res;
                }
                if (!schemaAllowsTypeLoose(schema, actualType)) return [];
                return [schema];
            };

            const getSchemaEnumValues = (schema) => {
                const values = [];
                if (Array.isArray(schema.enum)) values.push(...schema.enum);
                if (Object.prototype.hasOwnProperty.call(schema, 'const')) values.push(schema.const);
                return values;
            };

            const checkValueConstraints = (schema, actualType, rawValue, lineNumber, startColumn, endColumn, keyName) => {
                if (!schemaTools || !schema) return;
                if (actualType === 'dynamic') return;
                const parsed = parseYamlValue(rawValue);
                if (!parsed.ok) return;

                const candidates = collectConstraintSchemas(schema, actualType);
                if (candidates.length === 0) return;

                const hasConstraints = candidates.some((s) => (
                    (Array.isArray(s.enum) || Object.prototype.hasOwnProperty.call(s, 'const')) ||
                    (actualType === 'string' && typeof s.pattern === 'string')
                ));
                if (!hasConstraints) return;

                const hasUnconstrained = candidates.some((s) => (
                    !(Array.isArray(s.enum) || Object.prototype.hasOwnProperty.call(s, 'const')) &&
                    !(actualType === 'string' && typeof s.pattern === 'string')
                ));
                if (hasUnconstrained) return;

                const value = parsed.value;
                const matchesAny = candidates.some((s) => {
                    const enums = getSchemaEnumValues(s);
                    if (enums.length > 0 && !enums.some((v) => valueEquals(v, value))) return false;
                    if (actualType === 'string' && typeof s.pattern === 'string') {
                        try {
                            const re = new RegExp(s.pattern);
                            if (!re.test(String(value))) return false;
                        } catch (e) {
                            return true;
                        }
                    }
                    return true;
                });
                if (matchesAny) return;

                const enumValues = [];
                const patterns = [];
                for (const s of candidates) {
                    enumValues.push(...getSchemaEnumValues(s));
                    if (actualType === 'string' && typeof s.pattern === 'string') patterns.push(s.pattern);
                }
                const enumLabel = unique(enumValues).map((v) => toYamlScalar(v)).join(', ');
                const patternLabel = unique(patterns).join(' | ');

                let message;
                const label = keyName ? `"${keyName}"` : 'value';
                if (enumValues.length && patterns.length) {
                    message = `Value for ${label} must be one of: ${enumLabel}; or match pattern: ${patternLabel}`;
                } else if (enumValues.length) {
                    message = `Value for ${label} must be one of: ${enumLabel}`;
                } else if (patterns.length) {
                    message = `Value for ${label} must match pattern: ${patternLabel}`;
                } else {
                    return;
                }

                pushMarker({
                    severity: monaco.MarkerSeverity.Error,
                    message,
                    startLineNumber: lineNumber,
                    startColumn,
                    endLineNumber: lineNumber,
                    endColumn: Math.max(startColumn + 1, endColumn),
                });
            };

            for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
                let line = model.getLineContent(lineNumber);
                if (!line.trim()) continue;

                line = stripInlineComment(line).trimEnd();
                if (!line.trim()) continue;

                const indent = countIndent(line);
                if (blockScalarParentIndent !== null) {
                    if (indent <= blockScalarParentIndent) {
                        blockScalarParentIndent = null;
                    } else {
                        continue; // treat as block scalar content
                    }
                }

                if (indent === 0 && (hasTopLevelKey || hasTopLevelList)) {
                    const trimmed = line.trim();
                    if (trimmed !== '---' && trimmed !== '...' && !trimmed.startsWith('#')) {
                        const listItem0 = parseListItem(line);
                        const kv0 = parseKey(line);
                        const flow0 = trimmed.startsWith('{') || trimmed.startsWith('[');
                        if (!flow0 && !listItem0 && !kv0) {
                            markLineError(lineNumber, 'YAML: unexpected content at document root');
                            continue;
                        }
                    }
                }

                const listItem = parseListItem(line);
                if (listItem) {
                    if (listItem.indent > 0 && (hasTopLevelKey || hasTopLevelList) && stack.length === 1) {
                        markLineError(lineNumber, 'YAML: unexpected indentation at document root', 1);
                    }
                    while (stack.length > 1 && listItem.indent <= stack[stack.length - 1].indent) stack.pop();

                    const parent = stack[stack.length - 1];
                    checkChildIndent(parent, listItem.indent, lineNumber);
                    setActualType(parent, 'array', lineNumber);

                    const itemSchema = schemaTools ? schemaTools.getArrayItemSchema(parent.schema) : null;
                    const itemExpected = getExpectedContainerType(itemSchema);

                    const itemCtx = {
                        indent: listItem.indent,
                        schema: itemSchema,
                        expected: itemExpected,
                        actual: null,
                        keys: new Map(),
                        childIndent: null,
                        origin: {lineNumber, startColumn: listItem.dashIndex + 1, endColumn: listItem.dashIndex + 2},
                        reportedTypeMismatch: false,
                    };
                    stack.push(itemCtx);

                    if (!listItem.rest) continue;
                    if (isBlockScalarHeader(listItem.rest)) {
                        blockScalarParentIndent = listItem.indent;
                        checkValueType(itemSchema, 'string', lineNumber, listItem.afterDashIndex + 1, line.length + 1, null);
                        continue;
                    }

                    const inline = parseKey(' '.repeat(listItem.afterDashIndex) + listItem.rest);
                    if (inline) {
                        // handle inline mapping in the same line: "- key: value"
                        const kv = inline;
                        while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();
                        const ctx = stack[stack.length - 1];
                        checkChildIndent(ctx, kv.indent, lineNumber);
                        setActualType(ctx, 'object', lineNumber);

                        const prev = ctx.keys.get(kv.key);
                        if (prev) {
                            pushMarker({
                                severity: monaco.MarkerSeverity.Warning,
                                message: `Duplicate key "${kv.key}" (previous at line ${prev.lineNumber})`,
                                startLineNumber: lineNumber,
                                startColumn: kv.keyStartIndex + 1,
                                endLineNumber: lineNumber,
                                endColumn: kv.keyEndIndex + 1,
                            });
                        } else {
                            ctx.keys.set(kv.key, {lineNumber});
                        }

                        if (!kv.isQuoted && isNumberLike(kv.rawKey.trim())) {
                            pushMarker({
                                severity: monaco.MarkerSeverity.Error,
                                message: `Map keys must be strings. Quote numeric key as '${kv.rawKey}':`,
                                startLineNumber: lineNumber,
                                startColumn: kv.keyStartIndex + 1,
                                endLineNumber: lineNumber,
                                endColumn: kv.keyEndIndex + 1,
                            });
                        }

                        const propSchema = schemaTools ? schemaTools.getPropertySchema(ctx.schema, kv.key) : null;
                        if (isBlockScalarHeader(kv.after)) {
                            blockScalarParentIndent = kv.indent;
                        }
                        if (kv.isContainer) {
                            stack.push({
                                indent: kv.indent,
                                schema: propSchema,
                                expected: getExpectedContainerType(propSchema),
                                actual: null,
                                keys: new Map(),
                                childIndent: null,
                                origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1},
                                reportedTypeMismatch: false,
                            });
                        } else if (propSchema) {
                            const valueText = kv.after.trim();
                            const actual = classifyYamlScalar(valueText).type;
                            const valueStartColumn = kv.valueStartIndex + 1 + (kv.after.length - kv.after.trimStart().length);
                            checkValueType(propSchema, actual, lineNumber, valueStartColumn, line.length + 1, kv.key);
                            checkValueConstraints(propSchema, actual, valueText, lineNumber, valueStartColumn, line.length + 1, kv.key);
                        }
                        continue;
                    }

                    const scalar = classifyYamlScalar(listItem.rest).type;
                    checkValueType(itemSchema, scalar, lineNumber, listItem.afterDashIndex + 1, line.length + 1, null);
                    checkValueConstraints(itemSchema, scalar, listItem.rest, lineNumber, listItem.afterDashIndex + 1, line.length + 1, null);
                    continue;
                }

                const kv = parseKey(line);
                if (!kv) {
                    markLineError(lineNumber, 'YAML: expected a map key (key:) or list item (-)', indent + 1);
                    continue;
                }
                if (kv.indent > 0 && (hasTopLevelKey || hasTopLevelList) && stack.length === 1) {
                    markLineError(lineNumber, 'YAML: unexpected indentation at document root', 1);
                }

                while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();
                const ctx = stack[stack.length - 1];
                checkChildIndent(ctx, kv.indent, lineNumber);
                setActualType(ctx, 'object', lineNumber);

                const prev = ctx.keys.get(kv.key);
                if (prev) {
                    pushMarker({
                        severity: monaco.MarkerSeverity.Warning,
                        message: `Duplicate key "${kv.key}" (previous at line ${prev.lineNumber})`,
                        startLineNumber: lineNumber,
                        startColumn: kv.keyStartIndex + 1,
                        endLineNumber: lineNumber,
                        endColumn: kv.keyEndIndex + 1,
                    });
                } else {
                    ctx.keys.set(kv.key, {lineNumber});
                }

                if (!kv.isQuoted && isNumberLike(kv.rawKey.trim())) {
                    pushMarker({
                        severity: monaco.MarkerSeverity.Error,
                        message: `Map keys must be strings. Quote numeric key as '${kv.rawKey}':`,
                        startLineNumber: lineNumber,
                        startColumn: kv.keyStartIndex + 1,
                        endLineNumber: lineNumber,
                        endColumn: kv.keyEndIndex + 1,
                    });
                }

                const propSchema = schemaTools ? schemaTools.getPropertySchema(ctx.schema, kv.key) : null;
                if (kv.isContainer) {
                    stack.push({
                        indent: kv.indent,
                        schema: propSchema,
                        expected: getExpectedContainerType(propSchema),
                        actual: null,
                        keys: new Map(),
                        childIndent: null,
                        origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1},
                        reportedTypeMismatch: false,
                    });
                    continue;
                }

                if (isBlockScalarHeader(kv.after)) {
                    blockScalarParentIndent = kv.indent;
                }

                if (!propSchema) continue;

                const actual = classifyYamlScalar(kv.after).type;
                const valueStartColumn = kv.valueStartIndex + 1 + (kv.after.length - kv.after.trimStart().length);
                checkValueType(propSchema, actual, lineNumber, valueStartColumn, line.length + 1, kv.key);
                checkValueConstraints(propSchema, actual, kv.after, lineNumber, valueStartColumn, line.length + 1, kv.key);
            }

            return markers;
        };

        let schemaTools = null;
        let completionProvider = null;
        let hoverProvider = null;

        const scheduleLint = (() => {
            let handle = null;
            return () => {
                if (handle) clearTimeout(handle);
                handle = setTimeout(() => {
                    const model = editor.getModel();
                    if (!model) return;
                    monaco.editor.setModelMarkers(model, 'yaml-lint', lintYamlModel(model, schemaTools));
                }, 250);
            };
        })();

        editor.onDidChangeModelContent(() => scheduleLint());

        const setupYamlHints = (schemaRoot) => {
            schemaTools = createSchemaTools(schemaRoot);
            scheduleLint();

            const {
                resolveRef,
                getObjectProperties,
                getPropertySchema,
                getValueSuggestions,
            } = schemaTools;

            const buildContextStack = (model, upToLineNumber) => {
                const stack = [{indent: -1, schema: schemaRoot}];

                for (let lineNumber = 1; lineNumber <= upToLineNumber; lineNumber++) {
                    let line = model.getLineContent(lineNumber);
                    if (!line.trim()) continue;

                    line = stripInlineComment(line).trimEnd();
                    if (!line.trim()) continue;

                    const listItem = parseListItem(line);
                    if (listItem) {
                        while (stack.length > 1 && listItem.indent <= stack[stack.length - 1].indent) stack.pop();
                        const parent = resolveRef(stack[stack.length - 1].schema);
                        if (parent && parent.type === 'array' && parent.items) {
                            stack.push({indent: listItem.indent, schema: resolveRef(parent.items)});
                        } else {
                            stack.push({indent: listItem.indent, schema: null});
                        }

                        const inline = listItem.rest ? parseKey(' '.repeat(listItem.contentIndent) + listItem.rest) : null;
                        if (inline && inline.isContainer) {
                            while (stack.length > 1 && inline.indent <= stack[stack.length - 1].indent) stack.pop();
                            const ctx = resolveRef(stack[stack.length - 1].schema);
                            const next = ctx ? getPropertySchema(ctx, inline.key) : null;
                            stack.push({indent: inline.indent, schema: next});
                        }
                        continue;
                    }

                    const kv = parseKey(line);
                    if (!kv) continue;
                    while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();
                    if (!kv.isContainer) continue;

                    const ctx = resolveRef(stack[stack.length - 1].schema);
                    const next = ctx ? getPropertySchema(ctx, kv.key) : null;
                    stack.push({indent: kv.indent, schema: next});
                }

                return stack;
            };

            if (completionProvider) completionProvider.dispose();
            completionProvider = monaco.languages.registerCompletionItemProvider('yaml', {
                triggerCharacters: [':', ' '],
                provideCompletionItems: (model, position) => {
                    const line = model.getLineContent(position.lineNumber);
                    const lineNoComment = stripInlineComment(line);
                    const lineNoCommentTrimmedEnd = lineNoComment.trimEnd();
                    const listItem = parseListItem(lineNoCommentTrimmedEnd);

                    const wordUntil = model.getWordUntilPosition(position);
                    const range = new monaco.Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);

                    const cursorIndex = position.column - 1;
                    let contentStartIndex = 0;
                    if (listItem) {
                        contentStartIndex = listItem.afterDashIndex;
                    } else {
                        contentStartIndex = countIndent(lineNoComment);
                    }

                    if (cursorIndex < contentStartIndex) return {suggestions: []};

                    const text = lineNoCommentTrimmedEnd.slice(contentStartIndex);
                    const cursorInText = cursorIndex - contentStartIndex;
                    const colonIndex = text.indexOf(':');
                    const isValueContext = colonIndex >= 0 && cursorInText > colonIndex;

                    const stack = buildContextStack(model, position.lineNumber - 1);

                    const effectiveIndent = listItem ? listItem.contentIndent : countIndent(lineNoComment);
                    while (stack.length > 1 && effectiveIndent <= stack[stack.length - 1].indent) stack.pop();

                    let contextSchema = resolveRef(stack[stack.length - 1].schema);

                    if (listItem && cursorIndex >= listItem.afterDashIndex && contextSchema && contextSchema.type === 'array') {
                        contextSchema = resolveRef(contextSchema.items);
                    }

                    if (!contextSchema) return {suggestions: []};

                    // Scalar array item (e.g. "- tcp4") - suggest values (enum/examples/default)
                    if (listItem && colonIndex === -1 && !isValueContext) {
                        const props = getObjectProperties(contextSchema);
                        if (!props || Object.keys(props).length === 0) {
                            const values = getValueSuggestions(contextSchema);
                            const suggestions = values.map((v) => ({
                                label: toYamlScalar(v),
                                kind: monaco.languages.CompletionItemKind.Value,
                                insertText: toYamlScalar(v),
                                range,
                            }));
                            return {suggestions};
                        }
                    }

                    if (!isValueContext) {
                        const props = getObjectProperties(contextSchema);
                        const suggestions = Object.keys(props).map((key) => {
                            const s = resolveRef(props[key]);
                            const wantsArray = s && s.type === 'array';
                            const wantsBlock = s && (s.type === 'object' || wantsArray || s.properties);
                            const indent = listItem ? ' '.repeat(listItem.contentIndent) : ' '.repeat(countIndent(lineNoComment));
                            const innerIndent = indent + '  ';

                            const insertText = wantsArray ? `${key}:\n${indent}` : (wantsBlock ? `${key}:\n${innerIndent}` : `${key}: `);
                            const hasValueSuggestions = !wantsBlock && getValueSuggestions(s).length > 0;

                            return {
                                label: key,
                                kind: monaco.languages.CompletionItemKind.Property,
                                insertText,
                                insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
                                command: (wantsBlock || hasValueSuggestions) ? {id: 'editor.action.triggerSuggest'} : undefined,
                                documentation: s && s.description,
                                range,
                            };
                        });

                        return {suggestions};
                    }

                    const keyName = text.slice(0, colonIndex).trim();
                    const keySchema = getPropertySchema(contextSchema, keyName);
                    if (!keySchema) return {suggestions: []};

                    const values = getValueSuggestions(keySchema);
                    const suggestions = values.map((v) => ({
                        label: toYamlScalar(v),
                        kind: monaco.languages.CompletionItemKind.Value,
                        insertText: toYamlScalar(v),
                        range,
                    }));

                    return {suggestions};
                }
            });

            if (hoverProvider) hoverProvider.dispose();
            hoverProvider = monaco.languages.registerHoverProvider('yaml', {
                provideHover: (model, position) => {
                    if (!schemaTools) return null;

                    const line = model.getLineContent(position.lineNumber);
                    const lineNoComment = stripInlineComment(line).trimEnd();
                    const listItem = parseListItem(lineNoComment);

                    const cursorIndex = position.column - 1;
                    if (listItem && cursorIndex < listItem.afterDashIndex) return null;

                    let kv;
                    let effectiveIndent;
                    let keyStartIndex;
                    let keyEndIndex;

                    if (listItem) {
                        if (!listItem.rest) return null;
                        const synthetic = ' '.repeat(listItem.contentIndent) + listItem.rest;
                        kv = parseKey(synthetic);
                        if (!kv) return null;
                        effectiveIndent = listItem.contentIndent;
                        keyStartIndex = listItem.afterDashIndex + (kv.keyStartIndex - listItem.contentIndent);
                        keyEndIndex = listItem.afterDashIndex + (kv.keyEndIndex - listItem.contentIndent);
                    } else {
                        kv = parseKey(lineNoComment);
                        if (!kv) return null;
                        effectiveIndent = countIndent(lineNoComment);
                        keyStartIndex = kv.keyStartIndex;
                        keyEndIndex = kv.keyEndIndex;
                    }

                    if (cursorIndex < keyStartIndex || cursorIndex >= keyEndIndex) return null;

                    const stack = buildContextStack(model, position.lineNumber - 1);
                    while (stack.length > 1 && effectiveIndent <= stack[stack.length - 1].indent) stack.pop();

                    let contextSchema = resolveRef(stack[stack.length - 1].schema);
                    if (listItem && cursorIndex >= listItem.afterDashIndex && contextSchema && contextSchema.type === 'array') {
                        contextSchema = resolveRef(contextSchema.items);
                    }

                    if (!contextSchema) return null;
                    const propSchema = getPropertySchema(contextSchema, kv.key);
                    if (!propSchema) return null;

                    const resolved = resolveRef(propSchema);
                    const description = resolved && resolved.description;
                    if (!description) return null;

                    return {
                        range: new monaco.Range(
                            position.lineNumber,
                            keyStartIndex + 1,
                            position.lineNumber,
                            keyEndIndex + 1
                        ),
                        contents: [{value: description}],
                    };
                }
            });
        };

        const layout = () => {
            const top = container.getBoundingClientRect().top;
            container.style.height = `${Math.max(200, window.innerHeight - top)}px`;
            editor.layout();
        };
        window.addEventListener('resize', layout);
        layout();

        let dump;

        document.getElementById('save').addEventListener('click', async () => {
            let r = await fetch('api/config', {cache: 'no-cache'});
            if (r.ok && dump !== await r.text()) {
                alert('Config was changed from another place. Refresh the page and make changes again');
                return;
            }

            r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
            if (r.ok) {
                alert('OK');
                dump = editor.getValue();
                await fetch('api/restart', {method: 'POST'});
            } else {
                alert(await r.text());
            }
        });

        document.getElementById('suggest').addEventListener('click', () => {
            editor.trigger('source', 'editor.action.triggerSuggest', {});
        });

        (async () => {
            try {
                const r = await fetch('schema.json', {cache: 'no-cache'});
                if (r.ok) setupYamlHints(await r.json());
            } catch (e) {
                // ignore schema load errors
            }

            const r = await fetch('api/config', {cache: 'no-cache'});
            if (r.status === 410) {
                alert('Config file is not set');
            } else if (r.status === 404) {
                editor.setValue(''); // config file not exist
            } else if (r.ok) {
                dump = await r.text();
                editor.setValue(dump);
            } else {
                alert(`Unknown error: ${r.statusText} (${r.status})`);
            }
        })();
    });
</script>

</body>
</html>