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

import { QueryMarkdownBlockRenderer } from './renderer.js';
import type { MdBlockExecutionResult, MdTableCell, MdTableCellValue } from './types.js';

type MatrixStyleRuleKind = 'row' | 'column' | 'cell' | 'header';
type MatrixStyleRuleTarget = 'cell' | 'value';
type MatrixSelector = { kind: MatrixStyleRuleKind; condition?: string };
type CompiledMatrixStyleRule = {
    selectors: MatrixSelector[];
    target: MatrixStyleRuleTarget;
    style: Record<string, string>;
};

type MatrixRowContext = {
    index: number;
    name: string;
};

type MatrixColumnContext = {
    index: number;
    name: string;
};

type MatrixCellContext = {
    row: string;
    col: string;
    rowIndex: number;
    colIndex: number;
    value: string;
    datatype?: string;
    lang?: string;
};

type MatrixHeaderContext = {
    name: string;
};

type MatrixSelectorContext =
    | { kind: 'row'; context: MatrixRowContext }
    | { kind: 'column'; context: MatrixColumnContext }
    | { kind: 'cell'; context: MatrixCellContext }
    | { kind: 'header'; context: MatrixHeaderContext };

type MatrixCellRecord = {
    cell: MdTableCell;
};

type MatrixModel = {
    rowKeys: string[];
    columnKeys: string[];
    cells: Map<string, MatrixCellRecord>;
    rowHeaders: Map<string, MdTableCell>;
    columnHeaders: Map<string, MdTableCell>;
};

export class MatrixMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
    canRender(result: MdBlockExecutionResult): boolean {
        return result.status === 'ok'
            && result.format === 'table'
            && result.kind === 'matrix'
            && !!result.payload;
    }

    render(result: MdBlockExecutionResult): HTMLElement {
        const container = this.createResultContainer(result.status);
        container.classList.add('oml-md-result-table');

        const payloadColumns = result.payload?.columns ?? [];
        const payloadRows = result.payload?.rows ?? [];
        const rowIndex = payloadColumns.indexOf('row');
        const columnIndex = payloadColumns.indexOf('column');
        const valueIndex = payloadColumns.indexOf('value');
        if (rowIndex < 0 || columnIndex < 0 || valueIndex < 0) {
            container.appendChild(this.createMessageContainer(
                "Matrix renderer requires SELECT variables named 'row', 'column', and 'value'."
            ));
            return container;
        }

        const stylesheet = compileMatrixStylesheet(result.options);
        const rowColumnLabel = extractRowColumnLabel(result.options);
        const model = this.buildMatrixModel(payloadRows, result.payload?.rowsTyped ?? [], rowIndex, columnIndex, valueIndex);
        container.appendChild(this.renderMatrix(model, stylesheet, rowColumnLabel));
        return container;
    }

    private renderMatrix(
        model: MatrixModel,
        stylesheet: ReadonlyArray<CompiledMatrixStyleRule>,
        rowColumnLabel: string
    ): HTMLElement {
        const root = document.createElement('div');
        root.className = 'table-root graph-root matrix-root';

        const controls = document.createElement('div');
        controls.className = 'table-controls';
        const controlsLeft = document.createElement('div');
        controlsLeft.className = 'table-controls-left';
        const controlsRight = document.createElement('div');
        controlsRight.className = 'table-controls-right';

        const searchInput = document.createElement('input');
        searchInput.type = 'search';
        searchInput.className = 'tree-filter';
        searchInput.placeholder = 'Filter...';
        controlsRight.appendChild(searchInput);

        const downloadButton = createDownloadButton();
        controlsRight.appendChild(downloadButton);

        controls.appendChild(controlsLeft);
        controls.appendChild(controlsRight);
        root.appendChild(controls);

        const wrapper = document.createElement('div');
        wrapper.className = 'table-results-table-container hover-highlight';
        const table = document.createElement('div');
        table.className = 'table-results-table';
        const headerRow = document.createElement('div');
        headerRow.className = 'table-header-row';
        const body = document.createElement('div');
        body.className = 'table-body';
        wrapper.appendChild(table);
        root.appendChild(wrapper);

        const footer = document.createElement('div');
        footer.className = 'table-footer';
        const info = document.createElement('div');
        info.className = 'table-info-bar';
        footer.appendChild(info);
        root.appendChild(footer);

        const state = {
            search: '',
        };

        searchInput.addEventListener('input', () => {
            state.search = searchInput.value.trim().toLowerCase();
            renderMatrixPage();
        });

        downloadButton.addEventListener('click', () => {
            const visibility = this.computeVisibility(model, state.search);
            const csv = this.toCsv(model, visibility.visibleRows, visibility.visibleColumns, rowColumnLabel);
            this.requestTextFileDownload(csv, 'matrix', 'csv');
        });

        const renderMatrixPage = () => {
            const visibility = this.computeVisibility(model, state.search);
            const visibleRows = visibility.visibleRows;
            const visibleColumns = visibility.visibleColumns;

            headerRow.innerHTML = '';
            body.innerHTML = '';

            const gridTemplateColumns = this.buildGridTemplate(visibleColumns.length + 1);
            headerRow.style.gridTemplateColumns = gridTemplateColumns;

            const topLeft = document.createElement('div');
            topLeft.className = 'table-header-cell';
            topLeft.textContent = rowColumnLabel;
            this.applyStylesheet(topLeft, undefined, stylesheet, [
                { kind: 'header', context: { name: rowColumnLabel } }
            ]);
            headerRow.appendChild(topLeft);

            for (const [columnIndex, columnName] of visibleColumns.entries()) {
                const th = document.createElement('div');
                th.className = 'table-header-cell';
                const label = this.renderCellValue(columnName, model.columnHeaders.get(columnName));
                label.style.fontWeight = '400';
                th.appendChild(label);
                this.applyStylesheet(th, undefined, stylesheet, [
                    { kind: 'column', context: { index: columnIndex, name: columnName } }
                ]);
                headerRow.appendChild(th);
            }

            let rowCounter = 0;
            for (const rowName of visibleRows) {
                const rowElement = document.createElement('div');
                rowElement.className = 'table-row';
                rowElement.style.gridTemplateColumns = gridTemplateColumns;

                const rowHeaderCell = document.createElement('div');
                rowHeaderCell.className = 'table-cell';
                const rowHeaderValue = this.renderCellValue(rowName, model.rowHeaders.get(rowName));
                rowHeaderCell.appendChild(rowHeaderValue);
                const rowContext: MatrixRowContext = { index: rowCounter, name: rowName };
                this.applyStylesheet(rowHeaderCell, rowHeaderValue, stylesheet, [
                    { kind: 'row', context: rowContext },
                    { kind: 'cell', context: { row: rowName, col: 'row', rowIndex: rowCounter, colIndex: -1, value: rowName } }
                ]);
                rowElement.appendChild(rowHeaderCell);

                for (const [columnIndex, columnName] of visibleColumns.entries()) {
                    const key = buildMatrixKey(rowName, columnName);
                    const record = model.cells.get(key);
                    const recordCell = record?.cell;
                    const displayValue = this.formatCellDisplayText(recordCell?.value ?? '', recordCell);
                    const columnContext: MatrixColumnContext = { index: columnIndex, name: columnName };

                    const td = document.createElement('div');
                    td.className = 'table-cell';
                    const valueElement = this.renderCellValue(recordCell?.value ?? '', recordCell);
                    td.appendChild(valueElement);

                    const cellContext: MatrixCellContext = {
                        row: rowName,
                        col: columnName,
                        rowIndex: rowCounter,
                        colIndex: columnIndex,
                        value: displayValue,
                        datatype: undefined,
                        lang: undefined,
                    };
                    this.applyStylesheet(td, valueElement, stylesheet, [
                        { kind: 'row', context: rowContext },
                        { kind: 'column', context: columnContext },
                        { kind: 'cell', context: cellContext },
                    ]);
                    rowElement.appendChild(td);
                }

                body.appendChild(rowElement);
                rowCounter += 1;
            }

            table.replaceChildren(headerRow, body);
            info.textContent = `Showing rows ${visibleRows.length}/${model.rowKeys.length}, columns ${visibleColumns.length}/${model.columnKeys.length}`;
        };

        renderMatrixPage();
        return root;
    }

    private buildGridTemplate(totalColumnCount: number): string {
        const columns: string[] = [];
        for (let index = 0; index < totalColumnCount; index += 1) {
            columns.push('minmax(0, 1fr)');
        }
        return columns.join(' ');
    }

    private buildMatrixModel(
        rows: string[][],
        rowsTyped: MdTableCell[][],
        rowIndex: number,
        columnIndex: number,
        valueIndex: number
    ): MatrixModel {
        const rowKeys: string[] = [];
        const columnKeys: string[] = [];
        const seenRows = new Set<string>();
        const seenColumns = new Set<string>();
        const cells = new Map<string, MatrixCellRecord>();
        const rowHeaders = new Map<string, MdTableCell>();
        const columnHeaders = new Map<string, MdTableCell>();

        for (let index = 0; index < rows.length; index += 1) {
            const row = rows[index];
            const typedRow = rowsTyped[index] ?? [];
            const rowKey = row[rowIndex] ?? '';
            const columnKey = row[columnIndex] ?? '';
            const rowCell = typedRow[rowIndex] ?? literalCell(rowKey);
            const columnCell = typedRow[columnIndex] ?? literalCell(columnKey);
            const valueCell = typedRow[valueIndex] ?? literalCell(row[valueIndex] ?? '');
            if (!rowKey || !columnKey) {
                continue;
            }
            if (!seenRows.has(rowKey)) {
                seenRows.add(rowKey);
                rowKeys.push(rowKey);
                rowHeaders.set(rowKey, rowCell);
            }
            if (!seenColumns.has(columnKey)) {
                seenColumns.add(columnKey);
                columnKeys.push(columnKey);
                columnHeaders.set(columnKey, columnCell);
            }
            const key = buildMatrixKey(rowKey, columnKey);
            const record = cells.get(key) ?? { cell: { ...valueCell, values: valueCell.values.slice() } };
            mergeCellValues(record.cell, valueCell.values);
            cells.set(key, record);
        }

        return { rowKeys, columnKeys, cells, rowHeaders, columnHeaders };
    }

    private applyStylesheet(
        cellElement: HTMLElement,
        valueElement: HTMLElement | undefined,
        rules: ReadonlyArray<CompiledMatrixStyleRule>,
        contexts: ReadonlyArray<MatrixSelectorContext>
    ): void {
        for (const rule of rules) {
            if (rule.target !== 'value') {
                if (!matchesMatrixRule(rule, contexts)) {
                    continue;
                }
                for (const [property, cssValue] of Object.entries(rule.style)) {
                    cellElement.style.setProperty(property, cssValue);
                }
                continue;
            }
            if (!valueElement) {
                continue;
            }
            const valueTargets = resolveValueTargets(valueElement);
            for (const target of valueTargets) {
                const valueOverride = target.dataset.value ?? target.textContent ?? '';
                if (!matchesMatrixRule(rule, contexts, valueOverride)) {
                    continue;
                }
                for (const [property, cssValue] of Object.entries(rule.style)) {
                    target.style.setProperty(property, cssValue);
                }
            }
        }
    }

    private computeVisibility(model: MatrixModel, search: string): { visibleRows: string[]; visibleColumns: string[] } {
        if (!search) {
            return {
                visibleRows: model.rowKeys.slice(),
                visibleColumns: model.columnKeys.slice(),
            };
        }

        const allRows = model.rowKeys.slice();
        const allColumns = model.columnKeys.slice();
        const matchedRows = new Set<string>();
        const matchedColumns = new Set<string>();
        let rowLabelMatch = false;
        let columnLabelMatch = false;
        let cellMatch = false;

        for (const rowName of model.rowKeys) {
            if (rowName.toLowerCase().includes(search)) {
                matchedRows.add(rowName);
                rowLabelMatch = true;
            }
        }
        for (const columnName of model.columnKeys) {
            if (columnName.toLowerCase().includes(search)) {
                matchedColumns.add(columnName);
                columnLabelMatch = true;
            }
        }

        for (const rowName of model.rowKeys) {
            for (const columnName of model.columnKeys) {
                const record = model.cells.get(buildMatrixKey(rowName, columnName));
                if (!record) {
                    continue;
                }
                const cellText = this.formatCellDisplayText(record.cell.value, record.cell).toLowerCase();
                if (cellText.includes(search)) {
                    matchedRows.add(rowName);
                    matchedColumns.add(columnName);
                    cellMatch = true;
                }
            }
        }

        if (!rowLabelMatch && !columnLabelMatch && !cellMatch) {
            return { visibleRows: [], visibleColumns: [] };
        }
        if (rowLabelMatch && !columnLabelMatch && !cellMatch) {
            return {
                visibleRows: allRows.filter((rowName) => matchedRows.has(rowName)),
                visibleColumns: allColumns,
            };
        }
        if (!rowLabelMatch && columnLabelMatch && !cellMatch) {
            return {
                visibleRows: allRows,
                visibleColumns: allColumns.filter((columnName) => matchedColumns.has(columnName)),
            };
        }

        const visibleRows = model.rowKeys.filter((rowName) => matchedRows.has(rowName));
        const visibleColumns = model.columnKeys.filter((columnName) => matchedColumns.has(columnName));
        return { visibleRows, visibleColumns };
    }

    private toCsv(model: MatrixModel, visibleRows: string[], visibleColumns: string[], rowColumnLabel: string): string {
        const escapeCell = (value: string): string => `"${value.replace(/"/g, '""')}"`;
        const lines: string[] = [];
        lines.push([escapeCell(rowColumnLabel), ...visibleColumns.map(escapeCell)].join(','));
        for (const rowName of visibleRows) {
            const rowHeader = model.rowHeaders.get(rowName);
            const row: string[] = [escapeCell(this.formatCellDisplayText(rowHeader?.value ?? rowName, rowHeader))];
            for (const columnName of visibleColumns) {
                const record = model.cells.get(buildMatrixKey(rowName, columnName));
                row.push(escapeCell(this.formatCellDisplayText(record?.cell.value ?? '', record?.cell)));
            }
            lines.push(row.join(','));
        }
        return lines.join('\n');
    }
}

function extractRowColumnLabel(options: Record<string, unknown> | undefined): string {
    const raw = options?.rowColumnLabel;
    if (typeof raw !== 'string') {
        return 'row/column';
    }
    const trimmed = raw.trim();
    return trimmed.length > 0 ? trimmed : 'row/column';
}

function buildMatrixKey(rowKey: string, columnKey: string): string {
    return `${rowKey}\u0000${columnKey}`;
}

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

function mergeCellValues(target: MdTableCell, additions: ReadonlyArray<MdTableCellValue>): void {
    const existing = new Set(target.values.map((value) => `${value.kind}\u0000${value.value}`));
    for (const value of additions) {
        const key = `${value.kind}\u0000${value.value}`;
        if (existing.has(key)) {
            continue;
        }
        existing.add(key);
        target.values.push(value);
    }
    target.value = target.values.map((value) => {
        if (value.kind === 'bnode') {
            return `_:${value.value}`;
        }
        return value.value;
    }).join(', ');
}

function createDownloadButton(): HTMLButtonElement {
    const downloadButton = document.createElement('button');
    downloadButton.className = 'tree-download-btn';
    downloadButton.title = 'Download CSV';
    downloadButton.setAttribute('aria-label', 'Download CSV');
    const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    iconSvg.setAttribute('viewBox', '0 0 24 24');
    iconSvg.setAttribute('width', '20');
    iconSvg.setAttribute('height', '20');
    iconSvg.setAttribute('aria-hidden', 'true');
    iconSvg.setAttribute('focusable', 'false');
    iconSvg.style.fill = 'currentColor';
    const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    iconPath.setAttribute('d', 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z');
    iconSvg.appendChild(iconPath);
    downloadButton.appendChild(iconSvg);
    return downloadButton;
}

function compileMatrixStylesheet(options: Record<string, unknown> | undefined): CompiledMatrixStyleRule[] {
    const rawStylesheet = options?.stylesheet;
    if (!Array.isArray(rawStylesheet)) {
        return [];
    }
    const compiled: CompiledMatrixStyleRule[] = [];
    for (const rawRule of rawStylesheet) {
        if (!isRecord(rawRule)) {
            continue;
        }
        const selectors = parseSelectors(rawRule.selector);
        if (selectors.length === 0) {
            continue;
        }
        const style = normalizeStyle(rawRule.style);
        if (Object.keys(style).length === 0) {
            continue;
        }
        const target = rawRule.target === 'value' ? 'value' : 'cell';
        compiled.push({ selectors, target, style });
    }
    return compiled;
}

function parseSelectors(rawSelector: unknown): MatrixSelector[] {
    if (typeof rawSelector !== 'string') {
        return [];
    }
    const parts = splitSelectors(rawSelector);
    const selectors: MatrixSelector[] = [];
    for (const part of parts) {
        const parsed = parseSelector(part);
        if (parsed) {
            selectors.push(parsed);
        }
    }
    return selectors;
}

function splitSelectors(rawSelector: string): string[] {
    const parts: string[] = [];
    let current = '';
    let bracketDepth = 0;
    let quote: '"' | '\'' | undefined;
    for (let index = 0; index < rawSelector.length; index += 1) {
        const char = rawSelector[index];
        if (quote) {
            current += char;
            if (char === quote && rawSelector[index - 1] !== '\\') {
                quote = undefined;
            }
            continue;
        }
        if (char === '"' || char === '\'') {
            quote = char;
            current += char;
            continue;
        }
        if (char === '[') {
            bracketDepth += 1;
            current += char;
            continue;
        }
        if (char === ']' && bracketDepth > 0) {
            bracketDepth -= 1;
            current += char;
            continue;
        }
        if (char === ',' && bracketDepth === 0) {
            parts.push(current.trim());
            current = '';
            continue;
        }
        current += char;
    }
    if (current.trim()) {
        parts.push(current.trim());
    }
    return parts;
}

function parseSelector(raw: string): MatrixSelector | undefined {
    const trimmed = raw.trim();
    if (!trimmed) {
        return undefined;
    }
    const bracketStart = trimmed.indexOf('[');
    if (bracketStart === -1) {
        return toSelector(trimmed, undefined);
    }
    if (!trimmed.endsWith(']')) {
        return undefined;
    }
    const kind = trimmed.slice(0, bracketStart).trim();
    const condition = trimmed.slice(bracketStart + 1, -1).trim();
    return toSelector(kind, condition || undefined);
}

function toSelector(kind: string, condition: string | undefined): MatrixSelector | undefined {
    const normalized = kind.trim().toLowerCase();
    if (normalized !== 'row' && normalized !== 'column' && normalized !== 'cell' && normalized !== 'header') {
        return undefined;
    }
    return { kind: normalized, condition };
}

function normalizeStyle(rawStyle: unknown): Record<string, string> {
    if (!isRecord(rawStyle)) {
        return {};
    }
    const style: Record<string, string> = {};
    for (const [property, rawValue] of Object.entries(rawStyle)) {
        if (!property.trim()) {
            continue;
        }
        if (rawValue === undefined || rawValue === null) {
            continue;
        }
        style[property] = String(rawValue);
    }
    return style;
}

function matchesMatrixRule(
    rule: CompiledMatrixStyleRule,
    contexts: ReadonlyArray<MatrixSelectorContext>,
    cellValueOverride?: string
): boolean {
    return rule.selectors.some((selector) => {
        const contextEntry = contexts.find((entry) => entry.kind === selector.kind);
        if (!contextEntry) {
            return false;
        }
        if (selector.kind === 'cell' && cellValueOverride !== undefined) {
            const cellContext = {
                ...(contextEntry.context as MatrixCellContext),
                value: cellValueOverride,
            };
            return evaluateCondition(selector.condition, cellContext);
        }
        return evaluateCondition(selector.condition, contextEntry.context);
    });
}

function evaluateCondition(condition: string | undefined, context: object): boolean {
    if (!condition) {
        return true;
    }
    try {
        const scope = Object.assign(Object.create(null), context) as Record<string, unknown>;
        const keys = Object.keys(scope);
        const values = keys.map((key) => scope[key]);
        // eslint-disable-next-line no-new-func
        const evaluator = new Function(...keys, `"use strict"; return (${condition});`);
        return Boolean(evaluator(...values));
    } catch {
        return false;
    }
}

function resolveValueTargets(valueElement: HTMLElement): HTMLElement[] {
    const parts = Array.from(valueElement.querySelectorAll<HTMLElement>('.table-value-part'));
    if (parts.length > 0) {
        return parts;
    }
    return [valueElement];
}

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