// Copyright (c) 2026 Modelware. All rights reserved.

import { deriveSelectQueryFromShape, type OwlConstructResult, type OwlQueryResult, type OwlQueryTerm } from '@oml/owl';
import type { Term } from '@rdfjs/types';
import { displayLabelFromIri } from '../renderers/renderer.js';
import type {
    MdBlockExecutionResult,
    MdExecuteBlocksParams,
    MdExecuteBlocksResult,
    MdTableCell,
    MdTableCellValue
} from './md-execution.js';

export interface MarkdownExecutionBackend {
    ensureContext(modelUri: string): Promise<void>;
    resolveContextIri(modelUri: string): string;
    countContextQuads?: (modelUri: string) => Promise<number>;
    query(modelUri: string, sparql: string): Promise<OwlQueryResult>;
    construct(modelUri: string, sparql: string): Promise<OwlConstructResult>;
    isKnownOntologyIriNamespace?: (ontologyIri: string) => boolean;
}

export class MarkdownExecutor {
    constructor(private readonly backend: MarkdownExecutionBackend) {}

    async executeBlocks(params: MdExecuteBlocksParams): Promise<MdExecuteBlocksResult> {
        const resolvedModelUri = params.modelUri.trim();
        const contextQuadCount = this.backend.countContextQuads
            ? await this.backend.countContextQuads(resolvedModelUri)
            : undefined;
        await this.backend.ensureContext(resolvedModelUri);

        const results: MdBlockExecutionResult[] = [];
        for (const block of params.blocks) {
            try {
                if (block.kind === 'table' || block.kind === 'table-editor' || block.kind === 'chart') {
                    results.push(await this.executeTableBlock(block.kind, resolvedModelUri, block.id, block.source, block.options));
                    continue;
                }
                if (block.kind === 'matrix') {
                    results.push(await this.executeMatrixBlock(resolvedModelUri, block.id, block.source));
                    continue;
                }
                if (block.kind === 'tree') {
                    results.push(await this.executeTreeBlock(resolvedModelUri, block.id, block.source, block.options));
                    continue;
                }
                if (block.kind === 'graph') {
                    results.push(await this.executeGraphBlock(resolvedModelUri, block.id, block.source));
                    continue;
                }
                if (block.kind === 'diagram') {
                    results.push(await this.executeDiagramBlock(resolvedModelUri, block.id, block.source));
                    continue;
                }
                if (block.kind === 'text') {
                    results.push(await this.executeTextBlock(resolvedModelUri, block.id, block.source));
                    continue;
                }
                if (block.kind === 'list') {
                    results.push(await this.executeListBlock(resolvedModelUri, block.id, block.source));
                    continue;
                }
                results.push({
                    blockId: block.id,
                    kind: block.kind,
                    status: 'unimplemented',
                    format: 'message',
                    message: `${block.kind} blocks are not implemented yet.`
                });
            } catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                results.push({
                    blockId: block.id,
                    kind: block.kind,
                    status: 'error',
                    format: 'message',
                    message: message || `${block.kind} block execution failed.`
                });
            }
        }
        return { results, contextQuadCount };
    }

    private async executeMatrixBlock(
        modelUri: string | undefined,
        blockId: string,
        source: string
    ): Promise<MdBlockExecutionResult> {
        if (!modelUri) {
            return missingContext(blockId, 'matrix');
        }

        const sparql = preprocessQuerySource(this.backend, modelUri, source);
        const queryResult = await this.backend.query(modelUri, sparql);
        if (!queryResult.success) {
            return queryFailed(blockId, 'matrix', queryResult.error ?? 'SPARQL SELECT failed.');
        }

        const columns = extractSelectVariables(sparql);
        const groupConcatAliases = extractGroupConcatAliases(sparql);
        const columnSet = new Set<string>(columns);
        for (const row of queryResult.rows) {
            for (const key of row.keys()) {
                if (columnSet.has(key)) {
                    continue;
                }
                columnSet.add(key);
                columns.push(key);
            }
        }

        const rowsTyped = queryResult.rows.map((row) => columns.map((column) =>
            selectTermToCell(row.get(column), groupConcatAliases.get(column), this.backend.isKnownOntologyIriNamespace)
        ));
        const rows = rowsTyped.map((row) => row.map((cell) => tableCellToDisplayValue(cell)));
        return okTable(blockId, 'matrix', columns, rows, rowsTyped);
    }

    private async executeTableBlock(
        kind: 'table' | 'table-editor' | 'chart',
        modelUri: string | undefined,
        blockId: string,
        source: string,
        options?: Record<string, unknown>
    ): Promise<MdBlockExecutionResult> {
        if (!modelUri) {
            return missingContext(blockId, kind);
        }

        const preparedSource = preprocessQuerySource(this.backend, modelUri, source);
        const ownGraphIri = this.backend.resolveContextIri(modelUri);
        const derivedQuery = kind === 'table-editor'
            ? deriveSelectQueryFromShape(preparedSource, ownGraphIri)
            : undefined;
        const sparql = kind === 'table-editor'
            ? ensureOrderedByFirstVariable(derivedQuery?.sparql ?? preparedSource, derivedQuery?.columnVariables?.[0])
            : (derivedQuery?.sparql ?? preparedSource);
        if (kind === 'table-editor' && !derivedQuery) {
            return queryFailed(blockId, kind, 'Unable to derive SPARQL SELECT query from table-editor block shape.');
        }

        const queryResult = await this.backend.query(modelUri, sparql);
        if (!queryResult.success) {
            return queryFailed(blockId, kind, queryResult.error ?? 'SPARQL SELECT failed.');
        }

        const variableColumns = kind === 'table-editor'
            ? derivedQuery?.columnVariables.slice() ?? []
            : extractSelectVariables(sparql);
        const groupConcatAliases = extractGroupConcatAliases(sparql);
        const labelByVariable = kind === 'table-editor'
            ? (derivedQuery?.columnLabels ?? new Map<string, string>())
            : new Map<string, string>();
        const optionLabelByVariable = extractColumnLabelOverrides(options);
        for (const [variable, label] of optionLabelByVariable.entries()) {
            labelByVariable.set(variable, label);
        }
        const columns = variableColumns;
        const columnSet = new Set<string>(columns);
        for (const row of queryResult.rows) {
            for (const key of row.keys()) {
                if (columnSet.has(key)) {
                    continue;
                }
                columnSet.add(key);
                columns.push(key);
            }
        }

        const rowsTyped = queryResult.rows.map((row) => columns.map((column) =>
            selectTermToCell(row.get(column), groupConcatAliases.get(column), this.backend.isKnownOntologyIriNamespace)
        ));
        const rows = rowsTyped.map((row) => row.map((cell) => tableCellToDisplayValue(cell)));
        const displayColumns = columns.map((column) => labelByVariable.get(column) ?? column);
        return okTable(blockId, kind, displayColumns, rows, rowsTyped);
    }

    private async executeGraphBlock(
        modelUri: string | undefined,
        blockId: string,
        source: string
    ): Promise<MdBlockExecutionResult> {
        if (!modelUri) {
            return missingContext(blockId, 'graph');
        }

        const sparql = preprocessQuerySource(this.backend, modelUri, source);
        const constructResult = await this.backend.construct(modelUri, sparql);
        if (!constructResult.success) {
            return queryFailed(blockId, 'graph', constructResult.error ?? 'SPARQL CONSTRUCT failed.');
        }

        return constructRowsResult(blockId, 'graph', constructResult);
    }

    private async executeDiagramBlock(
        modelUri: string | undefined,
        blockId: string,
        source: string
    ): Promise<MdBlockExecutionResult> {
        if (!modelUri) {
            return missingContext(blockId, 'diagram');
        }

        const sparql = preprocessQuerySource(this.backend, modelUri, source);
        const constructResult = await this.backend.construct(modelUri, sparql);
        if (!constructResult.success) {
            return queryFailed(blockId, 'diagram', constructResult.error ?? 'SPARQL CONSTRUCT failed.');
        }

        return constructRowsResult(blockId, 'diagram', constructResult);
    }

    private async executeTreeBlock(
        modelUri: string | undefined,
        blockId: string,
        source: string,
        options?: Record<string, unknown>
    ): Promise<MdBlockExecutionResult> {
        if (!modelUri) {
            return missingContext(blockId, 'tree');
        }

        const sparql = preprocessQuerySource(this.backend, modelUri, source);
        const constructResult = await this.backend.construct(modelUri, sparql);
        if (!constructResult.success) {
            return queryFailed(blockId, 'tree', constructResult.error ?? 'SPARQL CONSTRUCT failed.');
        }

        const containmentNames = extractContainmentPropertyNames(options);
        const focusOrder: string[] = [];
        const focusSet = new Set<string>();
        const focusCellByKey = new Map<string, MdTableCell>();
        const valuesByFocus = new Map<string, Map<string, MdTableCellValue[]>>();
        const predicateToColumn = new Map<string, string>();
        const usedColumns = new Set<string>(['focus']);
        const columnOrder: string[] = [];
        const columnLabelByVariable = new Map<string, string>([['focus', 'focus']]);

        const addFocus = (focusCell: MdTableCell): void => {
            const key = treeFocusKey(focusCell);
            if (focusSet.has(key)) {
                return;
            }
            focusSet.add(key);
            focusOrder.push(key);
            focusCellByKey.set(key, focusCell);
            valuesByFocus.set(key, new Map<string, MdTableCellValue[]>());
        };
        const addCellValue = (focus: string, column: string, valueCell: MdTableCell): void => {
            const byColumn = valuesByFocus.get(focus);
            if (!byColumn) {
                return;
            }
            const values = byColumn.get(column) ?? [];
            const existing = new Set(values.map((value) => `${value.kind}\u0000${value.value}`));
            for (const value of valueCell.values) {
                const key = `${value.kind}\u0000${value.value}`;
                if (existing.has(key)) {
                    continue;
                }
                existing.add(key);
                values.push(value);
            }
            byColumn.set(column, values);
        };

        for (const quad of constructResult.quads) {
            const focusCell = termToTableCell(quad.subject as Term);
            const focus = treeFocusKey(focusCell);
            addFocus(focusCell);

            const predicateIri = quad.predicate.value;
            let column = predicateToColumn.get(predicateIri);
            if (!column) {
                const base = normalizeSparqlVariableName(localName(predicateIri) || 'value');
                column = uniqueVariableName(base, usedColumns);
                usedColumns.add(column);
                predicateToColumn.set(predicateIri, column);
                columnOrder.push(column);
                columnLabelByVariable.set(column, localName(predicateIri) || column);
            }

            const objectCell = termToTableCell(quad.object as Term);
            addCellValue(focus, column, objectCell);

            if (containmentNames.size > 0
                && (quad.object.termType === 'NamedNode' || quad.object.termType === 'BlankNode')
                && containmentNames.has(normalizePropertyIdentifier(predicateIri))) {
                addFocus(objectCell);
            }
        }

        const optionLabelByVariable = extractColumnLabelOverrides(options);
        for (const [variable, label] of optionLabelByVariable.entries()) {
            columnLabelByVariable.set(variable, label);
        }

        const columns = ['focus', ...columnOrder];
        const displayColumns = columns.map((column) => columnLabelByVariable.get(column) ?? column);
        const rowsTyped: MdTableCell[][] = focusOrder.map((focus) => {
            const byColumn = valuesByFocus.get(focus) ?? new Map<string, MdTableCellValue[]>();
            return columns.map((column): MdTableCell => {
                if (column === 'focus') {
                    return focusCellByKey.get(focus) ?? unknownCell(focus);
                }
                const values = byColumn.get(column) ?? [];
                if (values.length === 0) {
                    return unknownCell('');
                }
                const kind = values.every((value) => value.kind === 'iri')
                    ? 'iri'
                    : (values.every((value) => value.kind === 'bnode') ? 'bnode' : 'literal');
                const value = values.map((entry) => entry.kind === 'bnode' ? `_:${entry.value}` : entry.value).join(', ');
                return { kind, value, values, datatype: undefined, language: undefined };
            });
        });
        const rows = rowsTyped.map((row) => row.map((cell) => cell.kind === 'bnode' ? `_:${cell.value}` : cell.value));
        return okTable(blockId, 'tree', displayColumns, rows, rowsTyped);
    }

    private async executeTextBlock(
        modelUri: string | undefined,
        blockId: string,
        source: string
    ): Promise<MdBlockExecutionResult> {
        if (!modelUri) {
            return missingContext(blockId, 'text');
        }

        const sparql = preprocessQuerySource(this.backend, modelUri, source);
        const queryResult = await this.backend.query(modelUri, sparql);
        if (!queryResult.success) {
            return queryFailed(blockId, 'text', queryResult.error ?? 'SPARQL SELECT failed.');
        }

        const selected = extractSelectVariables(sparql);
        const firstVariable = selected[0] ?? queryResult.rows[0]?.keys().next().value;
        if (!firstVariable) {
            return queryFailed(blockId, 'text', 'Text block requires at least one SELECT variable.');
        }

        const columns = [firstVariable];
        const groupConcatAliases = extractGroupConcatAliases(sparql);
        const columnSet = new Set<string>(columns);
        for (const row of queryResult.rows) {
            for (const key of row.keys()) {
                if (columnSet.has(key)) {
                    continue;
                }
                columnSet.add(key);
                columns.push(key);
            }
        }
        const rowsTyped = queryResult.rows.map((row) => columns.map((column) =>
            selectTermToCell(row.get(column), groupConcatAliases.get(column), this.backend.isKnownOntologyIriNamespace)
        ));
        const rows = rowsTyped.map((row) => row.map((cell) => tableCellToDisplayValue(cell)));
        return okTable(blockId, 'text', columns, rows, rowsTyped);
    }

    private async executeListBlock(
        modelUri: string | undefined,
        blockId: string,
        source: string
    ): Promise<MdBlockExecutionResult> {
        if (!modelUri) {
            return missingContext(blockId, 'list');
        }

        const sparql = preprocessQuerySource(this.backend, modelUri, source);
        const queryResult = await this.backend.query(modelUri, sparql);
        if (!queryResult.success) {
            return queryFailed(blockId, 'list', queryResult.error ?? 'SPARQL SELECT failed.');
        }

        const selected = extractSelectVariables(sparql);
        const firstVariable = selected[0] ?? queryResult.rows[0]?.keys().next().value;
        if (!firstVariable) {
            return queryFailed(blockId, 'list', 'List block requires at least one SELECT variable.');
        }

        const columns = [firstVariable];
        const groupConcatAliases = extractGroupConcatAliases(sparql);
        const columnSet = new Set<string>(columns);
        for (const row of queryResult.rows) {
            for (const key of row.keys()) {
                if (columnSet.has(key)) {
                    continue;
                }
                columnSet.add(key);
                columns.push(key);
            }
        }
        const rowsTyped = queryResult.rows.map((row) => columns.map((column) =>
            selectTermToCell(row.get(column), groupConcatAliases.get(column), this.backend.isKnownOntologyIriNamespace)
        ));
        const rows = rowsTyped.map((row) => row.map((cell) => tableCellToDisplayValue(cell)));
        return okTable(blockId, 'list', columns, rows, rowsTyped);
    }
}

function preprocessQuerySource(backend: MarkdownExecutionBackend, modelUri: string, source: string): string {
    if (!source.includes('${ontologyIri}')) {
        return source;
    }
    return source.split('${ontologyIri}').join(backend.resolveContextIri(modelUri));
}

function missingContext(blockId: string, kind: MdBlockExecutionResult['kind']): MdBlockExecutionResult {
    return queryFailed(blockId, kind, 'No ontologyIri model is available for this markdown document.');
}

function queryFailed(blockId: string, kind: MdBlockExecutionResult['kind'], message: string): MdBlockExecutionResult {
    return {
        blockId,
        kind,
        status: 'error',
        format: 'message',
        message,
    };
}

function okTable(
    blockId: string,
    kind: MdBlockExecutionResult['kind'],
    columns: string[],
    rows: string[][],
    rowsTyped?: MdTableCell[][]
): MdBlockExecutionResult {
    return {
        blockId,
        kind,
        status: 'ok',
        format: 'table',
        payload: { columns, rows, rowsTyped },
    };
}

function constructRowsResult(
    blockId: string,
    kind: 'graph' | 'diagram',
    constructResult: OwlConstructResult
): MdBlockExecutionResult {
    return okTable(
        blockId,
        kind,
        ['subject', 'predicate', 'object'],
        constructResult.quads.map((quad) => [
            termToRawCellValue(quad.subject as Term),
            termToRawCellValue(quad.predicate as Term),
            termToRawCellValue(quad.object as Term),
        ]),
        constructResult.quads.map((quad) => [
            termToTableCell(quad.subject as Term),
            termToTableCell(quad.predicate as Term),
            termToTableCell(quad.object as Term),
        ]),
    );
}

function termToRawCellValue(term: Term): string {
    if (term.termType === 'NamedNode') {
        return term.value;
    }
    if (term.termType === 'BlankNode') {
        return `_:${term.value}`;
    }
    if (term.termType === 'Literal') {
        return term.value;
    }
    return String((term as { value?: unknown }).value ?? '');
}

function extractSelectVariables(sparql: string): string[] {
    const match = /select\s+(?:distinct\s+|reduced\s+)?([\s\S]*?)\bwhere\b/i.exec(sparql);
    if (!match) {
        return [];
    }
    const variables: string[] = [];
    for (const term of splitTopLevelProjectionTerms(match[1])) {
        const trimmed = term.trim();
        if (!trimmed) {
            continue;
        }
        const directVariable = /^\?([A-Za-z_][A-Za-z0-9_-]*)$/i.exec(trimmed);
        if (directVariable) {
            variables.push(`?${directVariable[1]}`);
            continue;
        }
        const alias = /\bas\s+\?([A-Za-z_][A-Za-z0-9_-]*)\b/i.exec(trimmed);
        if (alias) {
            variables.push(`?${alias[1]}`);
        }
    }
    const seen = new Set<string>();
    const result: string[] = [];
    for (const variable of variables) {
        const name = variable.slice(1);
        if (seen.has(name)) {
            continue;
        }
        seen.add(name);
        result.push(name);
    }
    return result;
}

function splitTopLevelProjectionTerms(rawProjection: string): string[] {
    const terms: string[] = [];
    let current = '';
    let depth = 0;
    let quote: '"' | '\'' | undefined;
    for (let index = 0; index < rawProjection.length; index += 1) {
        const char = rawProjection[index];
        if (quote) {
            current += char;
            if (char === quote && rawProjection[index - 1] !== '\\') {
                quote = undefined;
            }
            continue;
        }
        if (char === '"' || char === '\'') {
            quote = char;
            current += char;
            continue;
        }
        if (char === '(') {
            depth += 1;
            current += char;
            continue;
        }
        if (char === ')') {
            depth = Math.max(0, depth - 1);
            current += char;
            continue;
        }
        if (/\s/.test(char) && depth === 0) {
            if (current.trim()) {
                terms.push(current.trim());
                current = '';
            }
            continue;
        }
        current += char;
    }
    if (current.trim()) {
        terms.push(current.trim());
    }
    return terms;
}

function ensureOrderedByFirstVariable(sparql: string, firstVariable: string | undefined): string {
    const variable = (firstVariable ?? '').trim().replace(/^\?/, '');
    if (!variable || /\border\s+by\b/i.test(sparql)) {
        return sparql;
    }
    return `${sparql.trimEnd()}\nORDER BY ?${variable}`;
}

function extractColumnLabelOverrides(options: Record<string, unknown> | undefined): Map<string, string> {
    const labels = new Map<string, string>();
    const columns = options?.columns;
    if (!isPlainObject(columns)) {
        return labels;
    }
    for (const [variable, rawConfig] of Object.entries(columns)) {
        if (typeof rawConfig === 'string') {
            labels.set(variable, rawConfig);
            continue;
        }
        if (!isPlainObject(rawConfig)) {
            continue;
        }
        const label = rawConfig.label;
        if (typeof label === 'string' && label.trim().length > 0) {
            labels.set(variable, label);
        }
    }
    return labels;
}

function extractContainmentPropertyNames(options: Record<string, unknown> | undefined): Set<string> {
    const names = new Set<string>();
    const containment = options?.containment;
    if (!Array.isArray(containment)) {
        return names;
    }
    for (const entry of containment) {
        if (typeof entry !== 'string') {
            continue;
        }
        const normalized = normalizePropertyIdentifier(entry);
        if (normalized) {
            names.add(normalized);
        }
    }
    return names;
}

function normalizePropertyIdentifier(raw: string): string {
    const trimmed = raw.trim();
    if (!trimmed) {
        return '';
    }
    if (trimmed.includes(':')) {
        return trimmed.slice(trimmed.lastIndexOf(':') + 1).trim().toLowerCase();
    }
    return localName(trimmed).trim().toLowerCase();
}

function termToTableCell(term: Term): MdTableCell {
    if (term.termType === 'NamedNode') {
        return {
            kind: 'iri',
            value: term.value,
            values: [{ kind: 'iri', value: term.value }]
        };
    }
    if (term.termType === 'BlankNode') {
        return {
            kind: 'bnode',
            value: term.value,
            values: [{ kind: 'bnode', value: term.value }]
        };
    }
    if (term.termType === 'Literal') {
        return {
            kind: 'literal',
            value: term.value,
            values: [{ kind: 'literal', value: term.value, datatype: term.datatype?.value, language: term.language }],
            datatype: term.datatype?.value,
            language: term.language
        };
    }
    const value = String((term as { value?: unknown }).value ?? '');
    return {
        kind: 'unknown',
        value,
        values: [{ kind: 'unknown', value }]
    };
}

function treeFocusKey(cell: MdTableCell): string {
    if (cell.kind === 'bnode') {
        return `_:${cell.value}`;
    }
    return cell.value;
}

function unknownCell(value: string): MdTableCell {
    return {
        kind: 'unknown',
        value,
        values: [{ kind: 'unknown', value }]
    };
}

function selectTermToCell(
    term: OwlQueryTerm | undefined,
    groupConcatSeparator: string | undefined,
    isKnownOntologyIriNamespace?: (ontologyIri: string) => boolean
): MdTableCell {
    if (!term) {
        return { kind: 'unknown', value: '', values: [{ kind: 'unknown', value: '' }] };
    }
    if (term.termType === 'NamedNode') {
        return {
            kind: 'iri',
            value: term.value,
            values: [{ kind: 'iri', value: term.value }]
        };
    }
    if (term.termType === 'BlankNode') {
        return {
            kind: 'bnode',
            value: term.value,
            values: [{ kind: 'bnode', value: term.value }]
        };
    }
    if (term.termType === 'Literal' && groupConcatSeparator !== undefined) {
        const groupedValues = parseGroupConcatValues(term.value, groupConcatSeparator);
        const values = groupedValues.map((value) =>
            classifyGroupedValue(value, isKnownOntologyIriNamespace, term.datatype, term.language)
        );
        const kind = values.every((value) => value.kind === 'iri') ? 'iri' : 'literal';
        return { kind, value: term.value, values, datatype: term.datatype, language: term.language };
    }
    return {
        kind: 'literal',
        value: term.value,
        values: [{ kind: 'literal', value: term.value, datatype: term.datatype, language: term.language }],
        datatype: term.datatype,
        language: term.language
    };
}

function tableCellToDisplayValue(cell: MdTableCell): string {
    if (!Array.isArray(cell.values) || cell.values.length === 0) {
        return cell.kind === 'bnode' ? `_:${cell.value}` : cell.value;
    }
    if (typeof cell.value === 'string' && cell.value.length > 0) {
        return cell.kind === 'bnode' ? `_:${cell.value}` : cell.value;
    }
    return cell.values.map((entry) => entry.kind === 'bnode' ? `_:${entry.value}` : entry.value).join(' ');
}

function extractGroupConcatAliases(sparql: string): Map<string, string> {
    const aliases = new Map<string, string>();
    const match = /select\s+(?:distinct\s+|reduced\s+)?([\s\S]*?)\bwhere\b/i.exec(sparql);
    if (!match) {
        return aliases;
    }
    for (const term of splitTopLevelProjectionTerms(match[1])) {
        if (!/\bgroup_concat\s*\(/i.test(term)) {
            continue;
        }
        const alias = /\bas\s+\?([A-Za-z_][A-Za-z0-9_-]*)\b/i.exec(term);
        if (!alias?.[1]) {
            continue;
        }
        const separatorMatch = /\bseparator\s*=\s*["']([^"']*)["']/i.exec(term);
        aliases.set(alias[1], separatorMatch?.[1] ?? ' ');
    }
    return aliases;
}

function parseGroupConcatValues(raw: string, separator: string): string[] {
    if (separator.length === 0) {
        const trimmed = raw.trim();
        return trimmed ? [trimmed] : [];
    }
    const values = raw
        .split(separator)
        .map((entry) => entry.trim())
        .filter((entry) => entry.length > 0);
    return values;
}

function classifyGroupedValue(
    value: string,
    isKnownOntologyIriNamespace: ((ontologyIri: string) => boolean) | undefined,
    datatype?: string,
    language?: string
): MdTableCellValue {
    if (value.startsWith('_:')) {
        return { kind: 'bnode', value: value.slice(2) };
    }
    if (!isAbsoluteIri(value)) {
        return { kind: 'literal', value, datatype, language };
    }
    if (!isKnownOntologyIriNamespace) {
        return { kind: 'iri', value };
    }
    const key = ontologyLookupKeyFromIri(value);
    if (!key || !isKnownOntologyIriNamespace(key)) {
        return { kind: 'literal', value, datatype, language };
    }
    return { kind: 'iri', value };
}

function isAbsoluteIri(value: string): boolean {
    return /^[A-Za-z][A-Za-z0-9+.-]*:[^\s]+$/.test(value);
}

function ontologyLookupKeyFromIri(value: string): string | undefined {
    const iri = String(value ?? '').trim().replace(/^<|>$/g, '');
    if (!iri || !isAbsoluteIri(iri)) {
        return undefined;
    }
    const hashIndex = iri.indexOf('#');
    if (hashIndex >= 0) {
        return normalizeOntologyLookupKey(iri.slice(0, hashIndex + 1));
    }
    const schemeIndex = iri.indexOf('://');
    const slashIndex = iri.lastIndexOf('/');
    if (slashIndex > schemeIndex + 2) {
        return normalizeOntologyLookupKey(iri.slice(0, slashIndex + 1));
    }
    return normalizeOntologyLookupKey(iri);
}

function normalizeOntologyLookupKey(value: string): string {
    return value.trim().replace(/^<|>$/g, '').replace(/[\/#]+$/, '');
}

function normalizeSparqlVariableName(raw: string): string {
    const normalized = raw.replace(/[^A-Za-z0-9_]/g, '_');
    if (!normalized) {
        return 'value';
    }
    if (!/^[A-Za-z_]/.test(normalized)) {
        return `v_${normalized}`;
    }
    return normalized;
}

function uniqueVariableName(base: string, used: ReadonlySet<string>): string {
    if (!used.has(base)) {
        return base;
    }
    let index = 2;
    while (used.has(`${base}_${index}`)) {
        index += 1;
    }
    return `${base}_${index}`;
}

function localName(iri: string): string {
    return displayLabelFromIri(iri);
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
