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

import type { MdBlockExecutionResult, MdTableCell, MdTableCellValue } from './types.js';
import { appendInlineValue, createWikiLinkElement, isUrlValue, shortLabelFromIri } from './wikilink-utils.js';

export interface MarkdownBlockRenderer {
    canRender(result: MdBlockExecutionResult): boolean;
    render(result: MdBlockExecutionResult): HTMLElement;
}

let iriLabelSnapshot: ReadonlyMap<string, string> | undefined;

export function setIriLabelSnapshot(snapshot?: ReadonlyMap<string, string> | Record<string, string>): void {
    if (!snapshot) {
        iriLabelSnapshot = undefined;
        return;
    }
    if (snapshot instanceof Map) {
        iriLabelSnapshot = snapshot;
        return;
    }
    iriLabelSnapshot = new Map(
        Object.entries(snapshot).map(([iri, label]) => [normalizeIriLabelKey(iri), String(label)] as const)
    );
}

export function runWithIriLabelSnapshot<T>(
    snapshot: ReadonlyMap<string, string> | Record<string, string> | undefined,
    action: () => T
): T {
    const previous = iriLabelSnapshot;
    setIriLabelSnapshot(snapshot);
    try {
        return action();
    } finally {
        iriLabelSnapshot = previous;
    }
}

export function displayLabelFromIri(iri: string): string {
    const normalized = normalizeIriLabelKey(iri);
    if (!normalized) {
        return iri;
    }

    const labeled = iriLabelSnapshot?.get(normalized);
    if (typeof labeled === 'string' && labeled.length > 0) {
        return labeled;
    }

    const fragmentIndex = normalized.lastIndexOf('#');
    if (fragmentIndex >= 0 && fragmentIndex < normalized.length - 1) {
        return decodeIriLabelSegment(normalized.slice(fragmentIndex + 1));
    }

    const withoutQuery = normalized.split('?')[0]?.replace(/\/+$/, '') ?? normalized;
    const slashIndex = withoutQuery.lastIndexOf('/');
    if (slashIndex >= 0 && slashIndex < withoutQuery.length - 1) {
        return decodeIriLabelSegment(withoutQuery.slice(slashIndex + 1));
    }

    return normalized;
}

function normalizeIriLabelKey(iri: string): string {
    return String(iri ?? '').trim().replace(/^<|>$/g, '');
}

function decodeIriLabelSegment(value: string): string {
    try {
        return decodeURIComponent(value);
    } catch {
        return value;
    }
}

export type TableBlockKind = 'table' | 'table-editor' | 'matrix' | 'tree';
export type CanvasBlockKind = 'graph' | 'diagram';

export abstract class BaseMarkdownBlockRenderer implements MarkdownBlockRenderer {
    abstract canRender(result: MdBlockExecutionResult): boolean;
    abstract render(result: MdBlockExecutionResult): HTMLElement;

    protected createResultContainer(status: MdBlockExecutionResult['status']): HTMLElement {
        const container = document.createElement('div');
        container.className = `oml-md-result oml-md-result-${status}`;
        return container;
    }

    protected createMessageContainer(text: string): HTMLElement {
        const message = document.createElement('div');
        message.className = 'oml-md-result-message';
        message.textContent = text;
        return message;
    }

    protected requestTextFileDownload(content: string, fallbackName = 'download', fileExtension = 'txt'): void {
        const filename = this.buildDownloadFileName(fallbackName, fileExtension);
        const event = new CustomEvent('md-download-file', {
            detail: { filename, content },
        });
        window.dispatchEvent(event);
    }

    private buildDownloadFileName(fallbackName: string, fileExtension: string): string {
        const sourceUri = document.body.getAttribute('data-source-uri') ?? '';
        let namePart = fallbackName;
        if (sourceUri) {
            const fileName = sourceUri.split('/').pop() ?? '';
            const fileStem = fileName.replace(/\.[^.]+$/, '');
            if (fileStem) {
                namePart = fileStem;
            }
        }
        const sanitized = namePart.replace(/\W+/g, '_');
        const ts = new Date().toISOString().replace(/[:.]/g, '-');
        const extension = fileExtension.replace(/^\./, '') || 'csv';
        return `${sanitized}_${ts}.${extension}`;
    }
}

export abstract class QueryMarkdownBlockRenderer extends BaseMarkdownBlockRenderer {
    protected renderCellValue(raw: string, typedValue?: MdTableCell): HTMLElement {
        const value = document.createElement('span');
        value.className = 'table-value';
        const entries = typedValue?.values ?? [];
        if (entries.length > 0) {
            entries.forEach((entry, index) => {
                if (index > 0) {
                    value.appendChild(document.createTextNode(' '));
                }
                value.appendChild(this.renderValueToken(entry));
            });
            return value;
        }
        const display = this.formatCellDisplayText(raw, typedValue);
        const fragment = document.createElement('span');
        fragment.className = 'table-value-part';
        fragment.dataset.value = display;
        appendInlineValue(fragment, display);
        value.appendChild(fragment);
        return value;
    }

    protected formatCellDisplayText(raw: string, typedValue?: MdTableCell): string {
        if (typedValue?.values?.length) {
            return typedValue.values.map((entry) => {
                if (entry.kind === 'iri') {
                    return shortLabelFromIri(entry.value);
                }
                if (entry.kind === 'bnode') {
                    return `_:${entry.value}`;
                }
                return entry.value;
            }).join(' ');
        }
        return raw;
    }

    protected getBlockSource(result: MdBlockExecutionResult): string | undefined {
        return result.blockSource;
    }

    protected getBlockMeta(result: MdBlockExecutionResult): string | undefined {
        return result.blockMeta;
    }

    protected getBlockRange(result: MdBlockExecutionResult): { start?: number; end?: number } {
        return { start: result.blockLineStart, end: result.blockLineEnd };
    }

    private renderValueToken(entry: MdTableCellValue): HTMLElement {
        const fragment = document.createElement('span');
        fragment.className = 'table-value-part';
        fragment.dataset.value = entry.value;
        if (entry.kind === 'iri') {
            if (isBuiltInOntologyIri(entry.value)) {
                appendInlineValue(fragment, shortLabelFromIri(entry.value));
            } else {
                fragment.appendChild(createWikiLinkElement(entry.value, shortLabelFromIri(entry.value)));
            }
            return fragment;
        }
        if (entry.kind === 'bnode') {
            appendInlineValue(fragment, `_:${entry.value}`);
            return fragment;
        }
        if (isUrlValue(entry.value)) {
            const link = document.createElement('a');
            link.href = entry.value;
            link.textContent = entry.value;
            fragment.appendChild(link);
            return fragment;
        }
        appendInlineValue(fragment, entry.value);
        return fragment;
    }
}

export abstract class CanvasMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
    protected abstract readonly canvasKinds: ReadonlyArray<CanvasBlockKind>;

    canRender(result: MdBlockExecutionResult): boolean {
        return result.status === 'ok'
            && this.canvasKinds.includes(result.kind as CanvasBlockKind);
    }

    protected renderNotImplemented(result: MdBlockExecutionResult): HTMLElement {
        const container = this.createResultContainer(result.status);
        const source = this.getBlockSource(result);
        const sourceHint = source && source.trim().length > 0 ? ' Query source is available to this renderer.' : '';
        container.appendChild(this.createMessageContainer(`Renderer for '${result.kind}' is not implemented yet.${sourceHint}`));
        return container;
    }
}

const BUILT_IN_ONTOLOGY_IRIS = new Set([
    'http://opencaesar.io/oml',
    'http://www.w3.org/2001/XMLSchema',
    'http://www.w3.org/1999/02/22-rdf-syntax-ns',
    'http://www.w3.org/2000/01/rdf-schema',
    'http://www.w3.org/2002/07/owl',
    'http://www.w3.org/2003/11/swrl',
    'http://www.w3.org/2003/11/swrlb',
]);

function ontologyLookupKeyFromIri(value: string): string | undefined {
    const iri = String(value ?? '').trim().replace(/^<|>$/g, '');
    if (!iri || !/^[A-Za-z][A-Za-z0-9+.-]*:[^\s]+$/.test(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 isBuiltInOntologyIri(iri: string): boolean {
    const key = ontologyLookupKeyFromIri(iri);
    if (!key) {
        return false;
    }
    return BUILT_IN_ONTOLOGY_IRIS.has(normalizeOntologyLookupKey(key));
}
