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

import { QueryMarkdownBlockRenderer } from './renderer.js';
import type { MdBlockExecutionResult } from './types.js';
import { appendInlineValue } from './wikilink-utils.js';

type TextStyleSelectorKind = 'paragraph';
type TextStyleTarget = 'value';

type CompiledTextStyleRule = {
    selectors: Array<{ kind: TextStyleSelectorKind; condition?: string }>;
    target: TextStyleTarget;
    style: Record<string, string>;
};

type TextSelectorContext = {
    kind: 'paragraph';
    context: {
        index: number;
        value: string;
        row: { get: (key?: unknown) => { value: string } | undefined };
    };
};

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

    render(result: MdBlockExecutionResult): HTMLElement {
        const container = this.createResultContainer(result.status);
        container.classList.add('oml-md-result-plain');
        const payload = result.payload;
        if (!payload || payload.rows.length === 0) {
            container.appendChild(this.createMessageContainer('No results.'));
            return container;
        }
        const stylesheet = compileTextStylesheet(result.options);

        const wrapper = document.createElement('div');
        wrapper.className = 'oml-md-result-text';
        payload.rows.forEach((row, index) => {
            const paragraph = document.createElement('p');
            const valueSpan = document.createElement('span');
            valueSpan.className = 'oml-md-result-text-value';
            appendInlineValue(valueSpan, row[0] ?? '');
            paragraph.appendChild(valueSpan);

            const context = createParagraphSelectorContext(index, row[0] ?? '', payload.columns, row);
            for (const rule of stylesheet) {
                if (!matchesTextRule(rule, [context])) {
                    continue;
                }
                applyTextValueStyles(valueSpan, rule.style);
            }

            wrapper.appendChild(paragraph);
        });
        container.appendChild(wrapper);
        return container;
    }
}

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

function createParagraphSelectorContext(index: number, value: string, columns: string[], row: string[]): TextSelectorContext {
    return { kind: 'paragraph', context: { index, value, row: createRowAccessor(columns, row) } };
}

function matchesTextRule(rule: CompiledTextStyleRule, contexts: ReadonlyArray<TextSelectorContext>): boolean {
    return rule.selectors.some((selector) => {
        const contextEntry = contexts.find((entry) => entry.kind === selector.kind);
        if (!contextEntry) {
            return false;
        }
        return evaluateCondition(selector.condition, contextEntry.context);
    });
}

function applyTextValueStyles(target: HTMLElement, style: Record<string, string>): void {
    for (const [property, cssValue] of Object.entries(style)) {
        target.style.setProperty(property, cssValue);
    }
}

function parseTextSelectors(rawSelector: unknown): Array<{ kind: TextStyleSelectorKind; condition?: string }> {
    if (typeof rawSelector !== 'string') {
        return [];
    }
    const parts = splitSelectors(rawSelector);
    const selectors: Array<{ kind: TextStyleSelectorKind; condition?: string }> = [];
    for (const part of parts) {
        const parsed = parseTextSelector(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 parseTextSelector(selector: string): { kind: TextStyleSelectorKind; condition?: string } | undefined {
    const trimmed = selector.trim();
    if (!trimmed) {
        return undefined;
    }
    const bracketStart = trimmed.indexOf('[');
    if (bracketStart === -1) {
        return toTextSelector(trimmed, undefined);
    }
    if (!trimmed.endsWith(']')) {
        return undefined;
    }
    const kind = trimmed.slice(0, bracketStart).trim();
    const condition = trimmed.slice(bracketStart + 1, -1).trim();
    return toTextSelector(kind, condition || undefined);
}

function toTextSelector(kind: string, condition: string | undefined): { kind: TextStyleSelectorKind; condition?: string } | undefined {
    const normalized = kind.trim().toLowerCase();
    if (normalized !== 'paragraph') {
        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 createRowAccessor(columns: string[], row: string[]): { get: (key?: unknown) => { value: string } | undefined } {
    return {
        get: (key?: unknown) => {
            if (typeof key === 'number' && Number.isInteger(key)) {
                const value = row[key];
                return value === undefined ? undefined : { value };
            }
            if (typeof key === 'string') {
                const index = columns.indexOf(key);
                if (index < 0) {
                    return undefined;
                }
                const value = row[index];
                return value === undefined ? undefined : { value };
            }
            return undefined;
        }
    };
}

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 isRecord(value: unknown): value is Record<string, unknown> {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
