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

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

// --- Module-level Chart.js cache ---
let chartClass: (new (canvas: HTMLCanvasElement, config: unknown) => any) | undefined;

// --- Types ---

type ChartThemePalette = {
    foreground: string;
    background: string;
    border: string;
    muted: string;
    accent: string;
    accentForeground: string;
    fontFamily: string;
    isDark: boolean;
};

type DatasetDef = {
    label: string;
    dataColumn: string;
    extra: Record<string, unknown>;
    hasExplicitColors: boolean;
};

type ParsedChartOptions = {
    type: string;
    labelColumn: string;
    datasets: DatasetDef[];
    chartOptions: Record<string, unknown>;
    canvasHeight: string;
    canvasMinHeight: string;
};

// --- Renderer ---

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

    render(result: MdBlockExecutionResult): HTMLElement {
        const container = this.createResultContainer(result.status);
        const payload = result.payload;
        if (!payload) {
            container.appendChild(this.createMessageContainer('Chart renderer requires tabular payload.'));
            return container;
        }

        const parsed = parseChartOptions(result.options);
        if (!parsed.labelColumn) {
            container.appendChild(this.createMessageContainer(
                "Chart renderer requires 'data.labels' to be set to a column name."
            ));
            return container;
        }
        if (parsed.datasets.length === 0) {
            container.appendChild(this.createMessageContainer(
                "Chart renderer requires at least one entry in 'data.datasets'."
            ));
            return container;
        }

        const chartRoot = document.createElement('div');
        chartRoot.className = 'graph-canvas-root';
        chartRoot.style.height = parsed.canvasHeight;
        chartRoot.style.minHeight = parsed.canvasMinHeight;
        chartRoot.style.position = 'relative';
        container.appendChild(chartRoot);

        const canvasEl = document.createElement('canvas');
        chartRoot.appendChild(canvasEl);

        initializeChartWhenReady(
            chartRoot,
            canvasEl,
            payload.columns,
            payload.rows,
            parsed,
            container,
            (content) => this.requestTextFileDownload(content, 'chart', 'csv')
        );

        return container;
    }
}

// --- Initialization ---

function initializeChartWhenReady(
    chartRoot: HTMLElement,
    canvasEl: HTMLCanvasElement,
    columns: string[],
    rows: string[][],
    parsed: ParsedChartOptions,
    messageContainer: HTMLElement,
    downloadCsv: (content: string) => void,
): void {
    const maxAttempts = 20;
    let attempts = 0;
    const tryInit = (): void => {
        attempts += 1;
        if (!chartRoot.isConnected || chartRoot.clientWidth === 0 || chartRoot.clientHeight === 0) {
            if (attempts < maxAttempts) requestAnimationFrame(tryInit);
            return;
        }
        void doChartInit(chartRoot, canvasEl, columns, rows, parsed, downloadCsv).catch((err) => {
            const detail = err instanceof Error ? err.message : String(err);
            const msg = document.createElement('div');
            msg.className = 'oml-md-result-message';
            msg.textContent = `Chart rendering failed: ${detail}`;
            messageContainer.appendChild(msg);
        });
    };
    requestAnimationFrame(tryInit);
}

async function doChartInit(
    chartRoot: HTMLElement,
    canvasEl: HTMLCanvasElement,
    columns: string[],
    rows: string[][],
    parsed: ParsedChartOptions,
    downloadCsv: (content: string) => void,
): Promise<void> {
    const ChartCtor = await loadChartJs();
    let chart: any = null;

    const buildAndRender = (): void => {
        const theme = resolveThemePalette();
        const config = buildChartConfig(columns, rows, parsed, theme);
        if (chart) {
            chart.destroy();
        }
        chart = new ChartCtor(canvasEl, config);
    };

    buildAndRender();
    installChartToolbar(chartRoot, columns, rows, downloadCsv);

    // Resize the canvas whenever the host element resizes.
    const resizeObserver = new ResizeObserver(() => {
        if (!chartRoot.isConnected) return;
        chart?.resize();
    });
    resizeObserver.observe(chartRoot);

    // Also watch the result container so the chart fills width when page grows.
    const resultContainer = chartRoot.closest('.oml-md-result');
    let resultResizeObserver: ResizeObserver | undefined;
    if (resultContainer instanceof HTMLElement) {
        resultResizeObserver = new ResizeObserver(() => {
            const nextWidth = Math.max(0, Math.floor(resultContainer.clientWidth));
            chartRoot.style.width = `${nextWidth}px`;
            chart?.resize();
        });
        resultResizeObserver.observe(resultContainer);
    }

    chartRoot.addEventListener('DOMNodeRemovedFromDocument', () => {
        resizeObserver.disconnect();
        resultResizeObserver?.disconnect();
        chart?.destroy();
        chart = null;
    }, { once: true });

    // Rebuild chart on VS Code theme change so colors stay consistent.
    const themeObserver = new MutationObserver(() => {
        if (!chartRoot.isConnected) return;
        buildAndRender();
    });
    themeObserver.observe(document.body, {
        attributes: true,
        attributeFilter: ['class', 'style', 'data-vscode-theme-kind'],
    });
    themeObserver.observe(document.documentElement, {
        attributes: true,
        attributeFilter: ['class', 'style', 'data-vscode-theme-kind'],
    });
    const colorScheme = typeof window.matchMedia === 'function'
        ? window.matchMedia('(prefers-color-scheme: dark)')
        : undefined;
    const onColorSchemeChange = (): void => {
        if (!chartRoot.isConnected) return;
        buildAndRender();
    };
    colorScheme?.addEventListener?.('change', onColorSchemeChange);
    chartRoot.addEventListener('DOMNodeRemovedFromDocument', () => {
        themeObserver.disconnect();
        colorScheme?.removeEventListener?.('change', onColorSchemeChange);
    }, { once: true });

    // Drag-to-resize handle (same pattern as other canvas renderers).
    const parsedMinHeight = parseInt(chartRoot.style.minHeight, 10);
    const canvasMinHeight = Number.isFinite(parsedMinHeight) ? Math.max(120, parsedMinHeight) : 200;
    const resizeHandle = document.createElement('div');
    resizeHandle.className = 'canvas-resize-handle';
    chartRoot.appendChild(resizeHandle);
    let canvasResize: { pointerId: number; startY: number; startHeight: number } | undefined;
    const onResizePointerDown = (event: PointerEvent): void => {
        event.preventDefault();
        event.stopPropagation();
        if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
        canvasResize = { pointerId: event.pointerId, startY: event.clientY, startHeight: chartRoot.clientHeight };
        resizeHandle.setPointerCapture(event.pointerId);
    };
    const onResizePointerMove = (event: PointerEvent): void => {
        if (!canvasResize || event.pointerId !== canvasResize.pointerId) return;
        const delta = event.clientY - canvasResize.startY;
        const newHeight = Math.max(canvasMinHeight, canvasResize.startHeight + delta);
        chartRoot.style.height = `${Math.ceil(newHeight)}px`;
        chart?.resize();
    };
    const onResizePointerEnd = (event: PointerEvent): void => {
        if (!canvasResize || event.pointerId !== canvasResize.pointerId) return;
        canvasResize = undefined;
    };
    resizeHandle.addEventListener('pointerdown', onResizePointerDown);
    resizeHandle.addEventListener('pointermove', onResizePointerMove);
    resizeHandle.addEventListener('pointerup', onResizePointerEnd);
    resizeHandle.addEventListener('pointercancel', onResizePointerEnd);
}

function installChartToolbar(
    chartRoot: HTMLElement,
    columns: string[],
    rows: string[][],
    downloadCsv: (content: string) => void
): void {
    if (chartRoot.querySelector('.graph-corner-toolbar')) {
        return;
    }

    const hotspot = document.createElement('div');
    hotspot.className = 'graph-corner-hotspot';
    chartRoot.appendChild(hotspot);

    const toolbar = document.createElement('div');
    toolbar.className = 'graph-corner-toolbar';
    const downloadButton = createDownloadButton();
    downloadButton.addEventListener('click', () => {
        downloadCsv(toCsv(columns, rows));
    });
    toolbar.appendChild(downloadButton);
    chartRoot.appendChild(toolbar);

    let hideTimer = 0;
    const showToolbar = () => {
        if (hideTimer) {
            window.clearTimeout(hideTimer);
            hideTimer = 0;
        }
        chartRoot.classList.add('graph-toolbar-visible');
    };
    const scheduleHideToolbar = () => {
        hideTimer = window.setTimeout(() => {
            chartRoot.classList.remove('graph-toolbar-visible');
            hideTimer = 0;
        }, 120);
    };

    hotspot.addEventListener('mouseenter', showToolbar);
    toolbar.addEventListener('mouseenter', showToolbar);
    hotspot.addEventListener('mouseleave', scheduleHideToolbar);
    toolbar.addEventListener('mouseleave', scheduleHideToolbar);

    const swallowPointer = (event: Event) => {
        event.stopPropagation();
    };
    toolbar.addEventListener('pointerdown', swallowPointer, true);
    toolbar.addEventListener('mousedown', swallowPointer, true);
    downloadButton.addEventListener('pointerdown', swallowPointer, true);
    downloadButton.addEventListener('mousedown', swallowPointer, true);
}

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 toCsv(columns: string[], rows: string[][]): string {
    const escape = (value: string): string => `"${String(value ?? '').replace(/"/g, '""')}"`;
    const lines = [columns.map(escape).join(',')];
    for (const row of rows) {
        lines.push(columns.map((_, index) => escape(row[index] ?? '')).join(','));
    }
    return lines.join('\n');
}

// --- Chart.js loader ---

async function loadChartJs(): Promise<new (canvas: HTMLCanvasElement, config: unknown) => any> {
    if (chartClass) return chartClass;
    const mod = await import('chart.js/auto');
    const ctor = (mod as any).default ?? (mod as any).Chart;
    if (typeof ctor !== 'function') {
        throw new Error('Chart constructor is unavailable from chart.js/auto');
    }
    chartClass = ctor as new (canvas: HTMLCanvasElement, config: unknown) => any;
    return chartClass;
}

// --- Config builder ---

function buildChartConfig(
    columns: string[],
    rows: string[][],
    parsed: ParsedChartOptions,
    theme: ChartThemePalette,
): Record<string, unknown> {
    const labelColIdx = columns.indexOf(parsed.labelColumn);
    const labels = labelColIdx >= 0
        ? rows.map((row) => String(row[labelColIdx] ?? ''))
        : [];

    // Polar types get one auto-color per label; cartesian types get one per dataset.
    const isPolar = ['pie', 'doughnut', 'polarArea'].includes(parsed.type);
    const autoColorCount = isPolar ? labels.length : parsed.datasets.length;
    const autoColors = generateAutoColors(autoColorCount, theme.isDark);

    const datasets = parsed.datasets.map((def, datasetIdx) => {
        const dataColIdx = columns.indexOf(def.dataColumn);
        const data = dataColIdx >= 0
            ? rows.map((row) => {
                const v = row[dataColIdx];
                const n = Number(v);
                return Number.isFinite(n) ? n : (v ?? null);
            })
            : [];

        const resolvedExtra = deepResolveThemeTokens(def.extra, theme) as Record<string, unknown>;
        const ds: Record<string, unknown> = { label: def.label, data, ...resolvedExtra };

        if (!def.hasExplicitColors) {
            if (isPolar) {
                ds.backgroundColor = autoColors;
                ds.hoverOffset = 6;
            } else {
                const base = autoColors[datasetIdx % autoColors.length];
                ds.backgroundColor = withAlpha(base, 0.75);
                ds.borderColor = base;
                ds.borderWidth = 1;
            }
        }

        return ds;
    });

    // Base theme-aware Chart.js options.
    const baseOptions: Record<string, unknown> = {
        responsive: true,
        maintainAspectRatio: false,
        animation: { duration: 300 },
        color: theme.foreground,
        font: { family: theme.fontFamily },
        plugins: {
            title: {
                color: theme.foreground,
                font: { family: theme.fontFamily },
            },
            legend: {
                labels: {
                    color: theme.foreground,
                    font: { family: theme.fontFamily },
                },
            },
            tooltip: {
                bodyColor: theme.foreground,
                titleColor: theme.foreground,
                backgroundColor: theme.background,
                borderColor: theme.border,
                borderWidth: 1,
                bodyFont: { family: theme.fontFamily },
                titleFont: { family: theme.fontFamily },
            },
        },
    };

    if (!isPolar) {
        (baseOptions as any).scales = {
            x: {
                ticks: { color: theme.foreground, font: { family: theme.fontFamily } },
                title: { color: theme.foreground, font: { family: theme.fontFamily } },
                grid: { color: theme.border },
            },
            y: {
                ticks: { color: theme.foreground, font: { family: theme.fontFamily } },
                title: { color: theme.foreground, font: { family: theme.fontFamily } },
                grid: { color: theme.border },
            },
        };
    }

    const userOptions = deepResolveThemeTokens(parsed.chartOptions, theme) as Record<string, unknown>;
    const mergedOptions = deepMerge(baseOptions, userOptions);

    return {
        type: parsed.type,
        data: { labels, datasets },
        options: mergedOptions,
    };
}

// --- Auto-color generation ---

function generateAutoColors(count: number, isDark: boolean): string[] {
    if (count === 0) return [];
    const saturation = isDark ? 65 : 58;
    const lightness = isDark ? 58 : 48;
    return Array.from({ length: count }, (_, i) => {
        const hue = Math.round((i * 360) / count);
        return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
    });
}

function withAlpha(hslColor: string, alpha: number): string {
    const match = /^hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)$/.exec(hslColor);
    if (match) {
        return `hsla(${match[1]}, ${match[2]}%, ${match[3]}%, ${alpha})`;
    }
    return hslColor;
}

// --- Theme ---

function resolveThemePalette(): ChartThemePalette {
    const styles = getComputedStyle(document.documentElement);
    const foreground = readCssVarFirst(styles, ['--vscode-editor-foreground', '--oml-static-foreground'], '#24292f');
    const background = readCssVarFirst(styles, ['--vscode-editor-background', '--oml-static-background'], '#ffffff');
    const border = readCssVarFirst(styles, ['--vscode-editorWidget-border', '--oml-static-border'], '#d0d7de');
    const muted = readCssVarFirst(styles, ['--vscode-descriptionForeground', '--oml-static-muted'], '#57606a');
    const link = readCssVarFirst(styles, ['--vscode-textLink-foreground', '--oml-static-link'], '#0969da');
    const accent = readCssVarFirst(styles, ['--vscode-button-background'], link);
    const accentForeground = readCssVarFirst(styles, ['--vscode-button-foreground'], '#ffffff');
    const fontFamily = readCssVarFirst(
        styles,
        ['--vscode-editor-font-family'],
        '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
    );
    const kind = document.body.getAttribute('data-vscode-theme-kind') ?? '';
    const isDark = kind
        ? !kind.includes('light')
        : Boolean(window.matchMedia?.('(prefers-color-scheme: dark)').matches);
    return { foreground, background, border, muted, accent, accentForeground, fontFamily, isDark };
}

function readCssVarFirst(styles: CSSStyleDeclaration, names: string[], fallback: string): string {
    for (const name of names) {
        const value = styles.getPropertyValue(name).trim();
        if (value.length > 0) {
            return value;
        }
    }
    return fallback;
}

// --- Token resolution ---

function resolveThemeToken(value: string, theme: ChartThemePalette): string {
    const t = value.trim();
    if (t === 'theme.foreground') return theme.foreground;
    if (t === 'theme.background') return theme.background;
    if (t === 'theme.border') return theme.border;
    if (t === 'theme.muted') return theme.muted;
    if (t === 'theme.accent') return theme.accent;
    if (t === 'theme.accentForeground') return theme.accentForeground;
    return t;
}

function deepResolveThemeTokens(obj: unknown, theme: ChartThemePalette): unknown {
    if (typeof obj === 'string') return resolveThemeToken(obj, theme);
    if (Array.isArray(obj)) return obj.map((item) => deepResolveThemeTokens(item, theme));
    if (isRecord(obj)) {
        const result: Record<string, unknown> = {};
        for (const [key, val] of Object.entries(obj)) {
            result[key] = deepResolveThemeTokens(val, theme);
        }
        return result;
    }
    return obj;
}

// --- Option parsing ---

function parseChartOptions(options: Record<string, unknown> | undefined): ParsedChartOptions {
    const type = typeof options?.type === 'string' ? options.type.trim() : 'pie';
    const rawData = isRecord(options?.data) ? options.data : {};
    const labelColumn = typeof rawData.labels === 'string' ? rawData.labels.trim() : '';
    const rawDatasets = Array.isArray(rawData.datasets) ? rawData.datasets : [];

    const datasets: DatasetDef[] = [];
    for (const rawDs of rawDatasets) {
        if (!isRecord(rawDs)) continue;
        const dataColumn = typeof rawDs.data === 'string' ? rawDs.data.trim() : '';
        if (!dataColumn) continue;
        const label = typeof rawDs.label === 'string' ? rawDs.label : dataColumn;
        // Separate column-mapping keys from pass-through Chart.js dataset props.
        const { data: _d, label: _l, ...extra } = rawDs as Record<string, unknown>;
        const hasExplicitColors = 'backgroundColor' in extra || 'borderColor' in extra;
        datasets.push({ label, dataColumn, extra, hasExplicitColors });
    }

    const chartOptions = isRecord(options?.options) ? options.options : {};
    const canvas = isRecord(options?.canvas) ? options.canvas : {};
    const heightRaw = canvas.height ?? options?.height;
    const minHeightRaw = canvas.minHeight ?? options?.minHeight;

    return {
        type,
        labelColumn,
        datasets,
        chartOptions,
        canvasHeight: resolveHeight(heightRaw, '380px'),
        canvasMinHeight: resolveHeight(minHeightRaw, '200px'),
    };
}

function resolveHeight(raw: unknown, fallback: string): string {
    if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) return `${raw}px`;
    if (typeof raw === 'string') {
        const t = raw.trim();
        if (/^\d+(\.\d+)?(px|vh|%)$/.test(t)) return t;
    }
    return fallback;
}

// --- Utilities ---

function deepMerge(base: Record<string, unknown>, override: unknown): Record<string, unknown> {
    if (!isRecord(override)) return base;
    const result: Record<string, unknown> = { ...base };
    for (const [key, val] of Object.entries(override)) {
        if (val === undefined) continue;
        if (isRecord(val) && isRecord(result[key])) {
            result[key] = deepMerge(result[key] as Record<string, unknown>, val);
        } else {
            result[key] = val;
        }
    }
    return result;
}

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