"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildCartesianTooltipFormatter = exports.buildSqlRunnerCartesianTooltipFormatter = exports.isLineSeriesOption = exports.findPivotColumnFromSeriesRef = void 0;
exports.translatePivotRef = translatePivotRef;
exports.createStack100TooltipFormatter = createStack100TooltipFormatter;
exports.transformToPercentageStacking = transformToPercentageStacking;
const tslib_1 = require("tslib");
const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
const utc_1 = tslib_1.__importDefault(require("dayjs/plugin/utc"));
const lodash_1 = require("lodash");
const field_1 = require("../../types/field");
const savedCharts_1 = require("../../types/savedCharts");
const timeFrames_1 = require("../../types/timeFrames");
const formatting_1 = require("../../utils/formatting");
const sanitizeHtml_1 = require("../../utils/sanitizeHtml");
const types_1 = require("../types");
const tooltipStyles_1 = require("./styles/tooltipStyles");
const valueFormatter_1 = require("./valueFormatter");
dayjs_1.default.extend(utc_1.default);
/**
 * Translates user pivot refs (metric.dimension.value) to SQL pivot column names.
 * e.g., "orders_unique_order_count.orders_status.completed" -> "orders_unique_order_count_any_completed"
 *
 * User ref format: metric.dim1.val1 or metric.dim1.val1.dim2.val2...
 * Parts length must be odd (metric + pairs of dimension.value)
 */
function translatePivotRef(userRef, pivotValuesColumnsMap) {
    if (!pivotValuesColumnsMap)
        return undefined;
    const parts = userRef.split('.');
    if (parts.length < 3 || parts.length % 2 !== 1)
        return undefined;
    const metric = parts[0];
    const pivotPairs = [];
    for (let i = 1; i < parts.length; i += 2) {
        pivotPairs.push({ field: parts[i], value: parts[i + 1] });
    }
    const match = Object.entries(pivotValuesColumnsMap).find(([, column]) => {
        if (column.referenceField !== metric)
            return false;
        if (column.pivotValues.length !== pivotPairs.length)
            return false;
        return pivotPairs.every((pair) => column.pivotValues.some((pv) => pv.referenceField === pair.field &&
            String(pv.value) === pair.value));
    });
    return match?.[0];
}
/**
 * Extracts raw value from a row cell (handles both ResultRow and flat formats).
 */
function extractCellValue(cell) {
    if (cell && typeof cell === 'object' && 'value' in cell) {
        return cell.value?.raw;
    }
    return cell;
}
/**
 * Find the pivot column name for a simple metric reference by looking through series' pivotReference.
 * This allows users to write `${metric_name}` instead of the full `${metric_name.pivot_dim.pivot_value}` format.
 * Looks the matched reference up in pivotValuesColumnsMap to resolve the SQL pivot column name.
 *
 * @param ref - The simple metric field reference (e.g., "orders_amount_running_total")
 * @param params - The tooltip params array containing series information
 * @param series - The ECharts series array with pivotReference metadata
 * @param pivotValuesColumnsMap - Map of pivot column names to their metadata
 * @returns The pivot column name if found, undefined otherwise
 */
const findPivotColumnFromSeriesRef = (ref, params, series, pivotValuesColumnsMap, hiddenSeriesPivotRefs) => {
    if (!series)
        return undefined;
    // Extract the pivot values context from the first hovered series that has a pivotReference.
    // This tells us which group segment the user is hovering (e.g., "shipped").
    let hoveredPivotValues;
    for (const { seriesIndex } of params) {
        const s = typeof seriesIndex === 'number' ? series[seriesIndex] : undefined;
        if (s?.pivotReference?.pivotValues?.length) {
            hoveredPivotValues = s.pivotReference.pivotValues;
            break;
        }
    }
    // Collect all pivot references to search through: visible series + hidden series refs
    const pivotRefsToSearch = series
        .map((s) => s.pivotReference)
        .filter((pr) => !!pr);
    if (hiddenSeriesPivotRefs) {
        pivotRefsToSearch.push(...hiddenSeriesPivotRefs);
    }
    // Search all pivot references for one matching the requested ref AND whose
    // pivot values match the hovered context. This allows hidden series (e.g., a
    // table calculation with its chart series hidden) to be resolved in custom tooltips.
    const hovered = hoveredPivotValues ?? [];
    const matchingRef = pivotRefsToSearch.find((pivotRef) => {
        if (pivotRef.field !== ref)
            return false;
        const seriesPivotValues = pivotRef.pivotValues ?? [];
        return (seriesPivotValues.length === hovered.length &&
            seriesPivotValues.every((pv, i) => pv.value === hovered[i]?.value));
    });
    if (!matchingRef || !pivotValuesColumnsMap)
        return undefined;
    const pivotColumn = Object.entries(pivotValuesColumnsMap).find(([, col]) => col.referenceField === ref &&
        col.pivotValues.length === (matchingRef.pivotValues?.length ?? 0) &&
        col.pivotValues.every((pv, i) => pv.value === matchingRef.pivotValues?.[i]?.value));
    return pivotColumn?.[0];
};
exports.findPivotColumnFromSeriesRef = findPivotColumnFromSeriesRef;
/**
 * Compute a previous period date based on the current date, granularity, and offset
 */
const computePreviousPeriodDate = (currentDateStr, granularity, periodOffset) => {
    try {
        const currentDate = dayjs_1.default.utc(currentDateStr);
        if (!currentDate.isValid())
            return null;
        let previousDate;
        switch (granularity.toUpperCase()) {
            case timeFrames_1.TimeFrames.DAY:
                previousDate = currentDate.subtract(periodOffset, 'day');
                break;
            case timeFrames_1.TimeFrames.WEEK:
                previousDate = currentDate.subtract(periodOffset, 'week');
                break;
            case timeFrames_1.TimeFrames.MONTH:
                previousDate = currentDate.subtract(periodOffset, 'month');
                break;
            case timeFrames_1.TimeFrames.QUARTER:
                previousDate = currentDate.subtract(periodOffset * 3, 'month');
                break;
            case timeFrames_1.TimeFrames.YEAR:
                previousDate = currentDate.subtract(periodOffset, 'year');
                break;
            default:
                return null;
        }
        // Format based on granularity
        switch (granularity.toUpperCase()) {
            case timeFrames_1.TimeFrames.DAY:
            case timeFrames_1.TimeFrames.WEEK:
                return previousDate.format('MMM D, YYYY');
            case timeFrames_1.TimeFrames.MONTH:
                return previousDate.format('MMM YYYY');
            case timeFrames_1.TimeFrames.QUARTER: {
                // Format as "Q1 2024" style
                const quarter = Math.floor(previousDate.month() / 3) + 1;
                return `Q${quarter} ${previousDate.year()}`;
            }
            case timeFrames_1.TimeFrames.YEAR:
                return previousDate.format('YYYY');
            default:
                return previousDate.format('MMM YYYY');
        }
    }
    catch {
        return null;
    }
};
/**
 * Get the tooltip context
 * When series are bar stacked, then it's tuple mode(@reference to applyRoundedCornersToStackData), otherwise it's dataset mode.
 * @param params - The params
 * @param stackValue - The stack value
 * @param flipAxes - Whether the axes are flipped
 * @returns The tooltip context
 */
const getTooltipCtx = (params, stackValue, flipAxes) => {
    const v0 = params?.[0]?.value;
    const dataMode = v0 && typeof v0 === 'object' && !Array.isArray(v0)
        ? 'dataset'
        : 'tuple';
    let mode = 'plain';
    if (stackValue === types_1.StackType.PERCENT) {
        mode = 'stack100';
    }
    else if (stackValue) {
        mode = 'stackRegular';
    }
    return { flipAxes: !!flipAxes, mode, dataMode };
};
const getRawVal = (param, dim, yIdx, ctx) => {
    const v = param.value;
    if (Array.isArray(v)) {
        if (typeof yIdx === 'number')
            return v[yIdx];
        // ultimate fallback for tuple in regular stacks
        if (ctx.dataMode === 'tuple' && ctx.mode === 'stackRegular') {
            return ctx.flipAxes ? v[0] : v[1];
        }
        return undefined; // array but no valid index
    }
    if (v && typeof v === 'object' && dim) {
        // Only return the value if the key exists, otherwise undefined
        return v[dim];
    }
    // For non-object, non-array values (plain number/string), return as-is
    // But if we have a dim and v is an object, we already handled it above
    return typeof v === 'object' ? undefined : v;
};
/**
 * Get the header from the params
 * @param params - The params (accepts both TooltipFormatterParams and TooltipParam arrays)
 * @param itemsMap - Map of field IDs to field metadata (optional)
 * @param xFieldId - The x-axis field ID to get timeInterval from (optional)
 * @returns The header
 */
const getHeader = (params, itemsMap, xFieldId, timezone, displayTimezone) => {
    const firstParam = params[0];
    // Format the raw UTC value through the project timezone. axisValue is
    // undefined on some stack100 tooltip fires, so fall back to value[0] and
    // then to the dataset row.
    const rawAxisValue = firstParam?.axisValue;
    if (timezone && itemsMap && xFieldId) {
        const formatViaTz = (v) => (0, valueFormatter_1.getFormattedValue)(v, xFieldId, itemsMap, true, undefined, undefined, timezone);
        if (rawAxisValue != null)
            return formatViaTz(rawAxisValue);
        if (Array.isArray(firstParam?.value) && firstParam.value[0] != null) {
            return formatViaTz(firstParam.value[0]);
        }
        const datasetValue = firstParam?.value &&
            typeof firstParam.value === 'object' &&
            !Array.isArray(firstParam.value)
            ? firstParam.value[xFieldId]
            : undefined;
        if (datasetValue != null)
            return formatViaTz(datasetValue);
    }
    // When the x-axis values were shifted to project-timezone wall-clock,
    // axisValue and value[0] are already wall-clock (only need the offset
    // suffix), while the dataset row still holds the original UTC value
    // (needs the full conversion).
    if (displayTimezone && itemsMap && xFieldId) {
        if (rawAxisValue != null) {
            return (0, valueFormatter_1.getFormattedValue)(rawAxisValue, xFieldId, itemsMap, true, undefined, undefined, undefined, displayTimezone);
        }
        if (Array.isArray(firstParam?.value) && firstParam.value[0] != null) {
            return (0, valueFormatter_1.getFormattedValue)(firstParam.value[0], xFieldId, itemsMap, true, undefined, undefined, undefined, displayTimezone);
        }
        const datasetValue = firstParam?.value &&
            typeof firstParam.value === 'object' &&
            !Array.isArray(firstParam.value)
            ? firstParam.value[xFieldId]
            : undefined;
        if (datasetValue != null) {
            return (0, valueFormatter_1.getFormattedValue)(datasetValue, xFieldId, itemsMap, true, undefined, undefined, displayTimezone);
        }
    }
    // First try the standard axisValueLabel or name
    const standardHeader = firstParam?.axisValueLabel ?? firstParam?.name;
    // Check if ECharts failed to format (e.g., MIN/MAX metrics on date dimensions
    // have axis type 'value' instead of 'time', causing "Invalid Date")
    if (standardHeader) {
        return String(standardHeader);
    }
    // Fallback: try to format using the raw axis value
    // This handles cases where ECharts couldn't format the value properly
    if (rawAxisValue !== undefined && rawAxisValue !== null) {
        if (itemsMap && xFieldId) {
            return (0, valueFormatter_1.getFormattedValue)(rawAxisValue, xFieldId, itemsMap, true);
        }
        return String(rawAxisValue);
    }
    // For tuple mode (stacked bars) with time axis, extract x-value from data array
    if (firstParam?.value && Array.isArray(firstParam.value)) {
        // In tuple mode, first element is typically the x-axis value
        const xValue = firstParam.value[0];
        if (xValue !== undefined && xValue !== null) {
            // Use getFormattedValue for consistent formatting with axis labels
            if (itemsMap && xFieldId) {
                return (0, valueFormatter_1.getFormattedValue)(xValue, xFieldId, itemsMap, true, undefined, undefined, timezone);
            }
            return String(xValue);
        }
    }
    return '';
};
const extractColor = (marker) => {
    const s = typeof marker === 'string' ? marker : '';
    const m = s.match(/background-color:\s*([^;'"]+)/);
    return m ? m[1].trim() : '#000';
};
const unwrapValue = (v) => {
    if (v && typeof v === 'object') {
        if ('value' in v)
            return unwrapValue(v.value);
        if ('raw' in v)
            return v.raw;
        if ('formatted' in v)
            return v.formatted;
    }
    return v;
};
/**
 * Helper function to get the dimension from the encode
 */
const getDimFromEncodeAxis = (encode, dimensionNames, axis = 'y') => {
    if (!encode)
        return undefined;
    const e = encode[axis];
    if (typeof e === 'string')
        return e; // dataset: field hash
    if (Array.isArray(e) && typeof e[0] === 'number') {
        const idx = e[0];
        return dimensionNames && typeof idx === 'number'
            ? dimensionNames[idx]
            : undefined;
    }
    if (typeof e === 'number' && dimensionNames) {
        return dimensionNames[e];
    }
    return undefined;
};
const getValueIdxFromEncode = (encode, ctx, flipAxes) => {
    const axis = flipAxes ? 'x' : 'y';
    if (!encode) {
        let result;
        // tuple fallback for regular stacks
        if (ctx.dataMode === 'tuple' && ctx.mode === 'stackRegular') {
            result = flipAxes ? 0 : 1;
        }
        return result;
    }
    const e = encode[axis];
    if (typeof e === 'number')
        return e;
    if (Array.isArray(e) && typeof e[0] === 'number')
        return e[0];
    // tuple fallback for regular stacks
    if (ctx.dataMode === 'tuple' && ctx.mode === 'stackRegular') {
        return flipAxes ? 0 : 1;
    }
    return undefined;
};
const isLineSeriesOption = (obj) => typeof obj === 'object' && obj !== null && 'showSymbol' in obj;
exports.isLineSeriesOption = isLineSeriesOption;
/**
 * Creates a tooltip formatter for 100% stacked charts
 * This formatter shows both the percentage and the original count value
 *
 * @param originalValues - Map of x-axis values to their original y-values
 * @param getDimensionName - Function to extract the dimension name from a param
 * @param xAxisField - The x-axis field name to extract the raw x value from param data
 * @returns A formatter function compatible with ECharts tooltip
 *
 * @example
 * // SQL Runner usage
 * const formatter = createStack100TooltipFormatter(
 *     originalValues,
 *     (param) => {
 *         const { encode, dimensionNames } = param;
 *         const yFieldIndex = Array.isArray(encode.y) ? encode.y[0] : encode.y;
 *         return dimensionNames?.[yFieldIndex];
 *     },
 *     xFieldId
 * );
 *
 * @example
 * // Explorer usage with flip axes
 * const formatter = createStack100TooltipFormatter(
 *     originalValues,
 *     (param) => {
 *         const { encode, dimensionNames } = param;
 *         if (!dimensionNames || !encode) return undefined;
 *         return flipAxes ? dimensionNames[1] : dimensionNames[encode.y?.[0]];
 *     },
 *     xFieldId
 * );
 */
function createStack100TooltipFormatter(originalValues, getDimensionName, xAxisField, itemsMap, xAxisDateFormat, timezone, displayTimezone, pivotValuesColumnsMap, parameters) {
    return (params) => {
        if (!Array.isArray(params))
            return '';
        let header;
        const firstParam = params[0];
        if (xAxisDateFormat) {
            // Format from the raw data value to avoid timezone double-parsing.
            const rawDatasetValue = firstParam?.value?.[xAxisField];
            const rawValue = rawDatasetValue ?? firstParam?.axisValue;
            header =
                rawValue != null
                    ? (0, formatting_1.formatDateWithPattern)(rawValue, xAxisDateFormat)
                    : getHeader(params, itemsMap, xAxisField, timezone, displayTimezone);
        }
        else {
            header = getHeader(params, itemsMap, xAxisField, timezone, displayTimezone);
        }
        const rowsHtml = params
            .map((param) => {
            const { seriesName = '', marker = '' } = param;
            const dimensionName = getDimensionName(param);
            if (!dimensionName)
                return '';
            // Access value - try both data and value for compatibility
            // SQL Runner uses 'data', Explorer uses 'value'
            const valueObject = param.data ||
                (typeof param.value === 'object' &&
                    !Array.isArray(param.value)
                    ? param.value
                    : undefined);
            if (!valueObject || typeof valueObject !== 'object')
                return '';
            const percentage = valueObject[dimensionName];
            // Skip series rows that have no data at this x — happens on
            // gap rows inserted by padDatasetForContinuousAxis. Without
            // this guard the formatter would render a "0.0% (0)" line
            // per series at every gap date.
            if (percentage === undefined)
                return '';
            // Get the raw x-axis value from the data for originalValues lookup
            const rawXValue = String(valueObject[xAxisField] ?? '');
            const originalValue = originalValues?.get(rawXValue)?.get(dimensionName) || 0;
            // Format percentage and count
            const percentageStr = typeof percentage === 'number' && !Number.isNaN(percentage)
                ? `${percentage.toFixed(1)}%`
                : '0.0%';
            const countStr = 
            // eslint-disable-next-line no-nested-ternary
            typeof originalValue !== 'number'
                ? '0'
                : itemsMap
                    ? (0, valueFormatter_1.getFormattedValue)(originalValue, dimensionName, itemsMap, undefined, pivotValuesColumnsMap, parameters, timezone, displayTimezone)
                    : originalValue.toLocaleString();
            const colorIndicator = (0, tooltipStyles_1.formatColorIndicator)(extractColor(marker));
            return (0, tooltipStyles_1.formatCartesianTooltipRow)(colorIndicator, seriesName, (0, tooltipStyles_1.formatTooltipValue)(`${percentageStr} (${countStr})`));
        })
            .filter(Boolean)
            .join('');
        // No data at this x (e.g., gap row from continuous-axis padding):
        // suppress the tooltip entirely rather than showing an empty header.
        if (!rowsHtml)
            return '';
        const divider = (0, tooltipStyles_1.getTooltipDivider)();
        return `${(0, tooltipStyles_1.formatTooltipHeader)(header)}${divider}${rowsHtml}`;
    };
}
/**
 * Transform data for 100% stacked charts
 *
 * Converts absolute values to percentages where each stacked group totals 100%.
 * Also preserves original values for tooltip display.
 *
 * @param rows - Array of data rows to transform
 * @param xAxisField - Field reference for the x-axis (grouping key)
 * @param yFieldRefs - Array of field references for y-axis values to convert to percentages
 * @returns Object containing transformed data and original values map
 */
function transformToPercentageStacking(rows, xAxisField, yFieldRefs) {
    const originalValues = new Map();
    const totals = new Map();
    // Calculate totals for each x-axis value. Skip fields absent from the
    // row so gap rows (inserted by padDatasetForContinuousAxis) don't
    // contribute zeros to originalValues — that would surface as
    // "0.0% (0)" tooltip lines at gap dates.
    rows.forEach((row) => {
        const xValue = String(row[xAxisField]);
        let total = 0;
        yFieldRefs.forEach((yField) => {
            if (row[yField] === undefined)
                return;
            const value = (0, lodash_1.toNumber)(row[yField]) || 0;
            total += value;
            // Store original value
            if (!originalValues.has(xValue)) {
                originalValues.set(xValue, new Map());
            }
            originalValues.get(xValue).set(yField, value);
        });
        totals.set(xValue, total);
    });
    // Transform data to percentages. Preserve absence for gap-row yFields
    // so ECharts (and the tooltip) treat them as missing instead of 0%.
    const transformedResults = rows.map((row) => {
        const xValue = String(row[xAxisField]);
        const total = totals.get(xValue) || 1; // Avoid division by zero
        const newRow = { ...row };
        yFieldRefs.forEach((yField) => {
            if (row[yField] === undefined)
                return;
            const value = (0, lodash_1.toNumber)(row[yField]) || 0;
            newRow[yField] = (value / total) * 100;
        });
        return newRow;
    });
    return { transformedResults, originalValues };
}
/**
 * Build a simplified tooltip formatter for SQL Runner Cartesian charts
 * This formatter doesn't require itemsMap and works directly with the dataset
 */
const buildSqlRunnerCartesianTooltipFormatter = ({ stackValue, flipAxes, xFieldId, originalValues, xAxisDateFormat, }) => (params) => {
    if (!Array.isArray(params))
        return '';
    const ctx = getTooltipCtx(params, stackValue, flipAxes);
    // 100% stacks: use special formatter that shows percentage + original values
    if (ctx.mode === 'stack100' && xFieldId && originalValues) {
        return createStack100TooltipFormatter(originalValues, (param) => {
            const { encode, dimensionNames } = param;
            if (!dimensionNames || !encode)
                return undefined;
            if (flipAxes)
                return dimensionNames[1];
            const yIndex = Array.isArray(encode.y)
                ? encode.y[0]
                : encode.y;
            return typeof yIndex === 'number'
                ? dimensionNames[yIndex]
                : undefined;
        }, xFieldId, undefined, // No itemsMap available in SQL Runner
        xAxisDateFormat)(params);
    }
    let header;
    if (xAxisDateFormat && xFieldId) {
        // Format from the raw data value to avoid timezone double-parsing.
        // getHeader() returns an already-formatted string (e.g. "May 2024")
        // which, when re-parsed with moment().utc(), can shift months.
        const firstParam = params[0];
        const rawValue = firstParam?.value?.[xFieldId] ?? firstParam?.axisValue;
        header =
            rawValue != null
                ? (0, formatting_1.formatDateWithPattern)(rawValue, xAxisDateFormat)
                : getHeader(params, undefined, xFieldId);
    }
    else {
        header = getHeader(params, undefined, xFieldId);
    }
    // Build tooltip rows
    const rowsHtml = params
        .map((param) => {
        const { marker, seriesName, dimensionNames, encode } = param;
        const metricAxis = flipAxes ? 'x' : 'y';
        const dim = getDimFromEncodeAxis(encode, dimensionNames, metricAxis);
        const valueIdx = getValueIdxFromEncode(encode, ctx, !!flipAxes);
        const rawVal = getRawVal(param, dim, valueIdx, ctx);
        const valueForFormat = unwrapValue(rawVal);
        if (valueForFormat === undefined || valueForFormat === null)
            return '';
        // Format the value - for SQL Runner, we'll use the raw number
        const formattedValue = typeof valueForFormat === 'number'
            ? valueForFormat.toLocaleString()
            : String(valueForFormat);
        const colorIndicator = (0, tooltipStyles_1.formatColorIndicator)(extractColor(marker));
        return (0, tooltipStyles_1.formatCartesianTooltipRow)(colorIndicator, seriesName || '', (0, tooltipStyles_1.formatTooltipValue)(formattedValue));
    })
        .join('');
    const divider = (0, tooltipStyles_1.getTooltipDivider)();
    return `${(0, tooltipStyles_1.formatTooltipHeader)(header)}${divider}${rowsHtml}`;
};
exports.buildSqlRunnerCartesianTooltipFormatter = buildSqlRunnerCartesianTooltipFormatter;
/**
 * Sort tooltip params based on the tooltipSort configuration
 */
const sortTooltipParams = (params, tooltipSort, series, flipAxes, ctx) => {
    if (!tooltipSort || tooltipSort === savedCharts_1.TooltipSortByOptions.DEFAULT) {
        return params;
    }
    // Helper to extract numeric value from param for sorting
    const getNumericValue = (param) => {
        const { value, seriesIndex, encode, dimensionNames } = param;
        const seriesOption = typeof seriesIndex === 'number' ? series?.[seriesIndex] : undefined;
        const effectiveEncode = encode ?? seriesOption?.encode ?? undefined;
        // Try to get the value from the encode y-axis
        const metricAxis = flipAxes ? 'x' : 'y';
        const valueIdx = getValueIdxFromEncode(effectiveEncode, ctx, !!flipAxes);
        // For array values (tuple mode)
        if (Array.isArray(value)) {
            // Use the valueIdx if available, otherwise try index 1 (typical y-value position)
            let idx;
            if (valueIdx !== undefined) {
                idx = valueIdx;
            }
            else {
                idx = flipAxes ? 0 : 1;
            }
            const numVal = (0, lodash_1.toNumber)(value[idx]);
            if (!Number.isNaN(numVal))
                return numVal;
        }
        // For object values (dataset mode)
        if (value && typeof value === 'object' && !Array.isArray(value)) {
            // Try to find the metric dimension
            const dim = getDimFromEncodeAxis(effectiveEncode, dimensionNames, metricAxis) ?? dimensionNames?.[1];
            if (dim) {
                const val = value[dim];
                const numVal = (0, lodash_1.toNumber)(val);
                if (!Number.isNaN(numVal))
                    return numVal;
            }
            // Fallback: find any numeric value
            for (const v of Object.values(value)) {
                const numVal = (0, lodash_1.toNumber)(v);
                if (!Number.isNaN(numVal))
                    return numVal;
            }
        }
        return 0;
    };
    const sorted = [...params];
    switch (tooltipSort) {
        case savedCharts_1.TooltipSortByOptions.ALPHABETICAL:
            sorted.sort((a, b) => (a.seriesName || '').localeCompare(b.seriesName || ''));
            break;
        case savedCharts_1.TooltipSortByOptions.VALUE_ASCENDING:
            sorted.sort((a, b) => getNumericValue(a) - getNumericValue(b));
            break;
        case savedCharts_1.TooltipSortByOptions.VALUE_DESCENDING:
            sorted.sort((a, b) => getNumericValue(b) - getNumericValue(a));
            break;
        default:
            break;
    }
    return sorted;
};
const buildCartesianTooltipFormatter = ({ itemsMap, stackValue, flipAxes, xFieldId, originalValues, series, hiddenSeriesPivotRefs, tooltipHtmlTemplate, tooltipSort, pivotValuesColumnsMap, parameters, rows, timezone, displayTimezone, }) => (params) => {
    if (!Array.isArray(params) || !itemsMap)
        return '';
    const ctx = getTooltipCtx(params, stackValue, flipAxes);
    // 100% stacks: use special formatter that shows percentage + original values
    if (ctx.mode === 'stack100' && xFieldId && originalValues) {
        return createStack100TooltipFormatter(originalValues, (param) => {
            const { encode, dimensionNames } = param;
            if (!dimensionNames || !encode)
                return undefined;
            if (flipAxes)
                return dimensionNames[1];
            const yIndex = Array.isArray(encode.y)
                ? encode.y[0]
                : encode.y;
            return typeof yIndex === 'number'
                ? dimensionNames[yIndex]
                : undefined;
        }, xFieldId, itemsMap, undefined, timezone, displayTimezone, pivotValuesColumnsMap, parameters)(params);
    }
    const header = getHeader(params, itemsMap, xFieldId, timezone, displayTimezone);
    const sortedParams = sortTooltipParams(params, tooltipSort, series, flipAxes, ctx);
    // rows
    const rowsHtml = sortedParams
        .map((param) => {
        const { marker, seriesName, dimensionNames, encode, seriesIndex, } = param;
        const seriesOption = typeof seriesIndex === 'number'
            ? series?.[seriesIndex]
            : undefined;
        const effectiveEncode = encode ?? seriesOption?.encode ?? undefined;
        const yRefField = seriesOption?.encode?.yRef?.field ?? undefined;
        const yFieldHash = seriesOption?.encode?.yRef &&
            typeof seriesOption.encode.yRef === 'object'
            ? (0, savedCharts_1.hashFieldReference)(seriesOption.encode.yRef)
            : undefined;
        const seriesDimensionNames = Array.isArray(seriesOption?.dimensions)
            ? seriesOption?.dimensions.map((dimension) => typeof dimension === 'string'
                ? dimension
                : dimension?.name)
            : undefined;
        const metricAxis = flipAxes ? 'x' : 'y';
        const encodeDim = getDimFromEncodeAxis(effectiveEncode, dimensionNames, metricAxis) ??
            getDimFromEncodeAxis(effectiveEncode, seriesDimensionNames, metricAxis);
        const tooltipEncode = seriesOption?.encode?.tooltip;
        let tooltipDim;
        if (Array.isArray(tooltipEncode)) {
            // eslint-disable-next-line prefer-destructuring
            tooltipDim = tooltipEncode[0];
        }
        else if (typeof tooltipEncode === 'string') {
            tooltipDim = tooltipEncode;
        }
        const pivotDim = seriesOption?.pivotReference && pivotValuesColumnsMap
            ? Object.values(pivotValuesColumnsMap).find((column) => {
                const reference = seriesOption.pivotReference;
                if (!reference)
                    return false;
                if (column.referenceField !== reference.field)
                    return false;
                if (column.pivotValues.length !==
                    reference.pivotValues?.length)
                    return false;
                return column.pivotValues.every((pivotValue, index) => pivotValue.value ===
                    reference.pivotValues?.[index]?.value);
            })?.pivotColumnName
            : undefined;
        // Prefer the real field hash from encode.y (works for pivot)
        const dim = encodeDim ??
            tooltipDim ??
            pivotDim ??
            // final fallback: the second dimension name normally is the Y series
            param?.dimensionNames?.[1] ??
            undefined;
        const valueIdx = getValueIdxFromEncode(effectiveEncode, ctx, !!flipAxes);
        const rawValueKeys = [
            dim,
            pivotDim,
            tooltipDim,
            yFieldHash,
            yRefField,
        ].filter((k) => !!k && typeof k === 'string');
        // Extract raw cell value in both dataset/tuple modes
        let rawVal = getRawVal(param, dim, valueIdx, ctx);
        const needsPivotFallback = pivotDim &&
            dim &&
            pivotDim !== dim &&
            typeof param?.value === 'object' &&
            !Array.isArray(param?.value) &&
            param.value[dim] === undefined;
        if (needsPivotFallback ||
            (pivotDim && pivotDim !== dim && rawVal === param?.value)) {
            rawVal = getRawVal(param, pivotDim, valueIdx, ctx);
        }
        if ((rawVal === param?.value ||
            (rawVal &&
                typeof rawVal === 'object' &&
                !Array.isArray(rawVal))) &&
            rawValueKeys.length > 0) {
            for (const key of rawValueKeys) {
                const candidate = getRawVal(param, key, valueIdx, ctx);
                if (candidate !== undefined &&
                    candidate !== null &&
                    candidate !== param?.value &&
                    !(candidate === rawVal &&
                        typeof candidate === 'object')) {
                    rawVal = candidate;
                    break;
                }
                if (param?.value &&
                    typeof param.value === 'object' &&
                    !Array.isArray(param.value) &&
                    param.value[key] !==
                        undefined) {
                    rawVal =
                        param.value[key];
                    break;
                }
                if (param?.data &&
                    typeof param.data === 'object' &&
                    !Array.isArray(param.data) &&
                    param.data[key] !==
                        undefined) {
                    rawVal = param.data[key];
                    break;
                }
            }
        }
        const valueForFormat = unwrapValue(rawVal);
        if (valueForFormat === undefined || valueForFormat === null)
            return '';
        const formatKey = (typeof dim === 'string' &&
            pivotValuesColumnsMap?.[dim]?.referenceField) ??
            (pivotDim &&
                pivotValuesColumnsMap?.[pivotDim]?.referenceField) ??
            yRefField ??
            // Fallback to pivotReference.field when pivotValuesColumnsMap is unavailable
            seriesOption?.pivotReference?.field ??
            dim ??
            pivotDim ??
            '';
        // For period-over-period series, use the base field's format
        // using baseFieldId from metadata instead of string matching
        let effectiveFormatKey = formatKey;
        if (seriesOption?.periodOverPeriodMetadata?.baseFieldId) {
            const { baseFieldId } = seriesOption.periodOverPeriodMetadata;
            // Use base field format if it exists in itemsMap
            if (itemsMap[baseFieldId]) {
                effectiveFormatKey = baseFieldId;
            }
        }
        const formattedValue = (0, valueFormatter_1.getFormattedValue)(valueForFormat, effectiveFormatKey, itemsMap, undefined, pivotValuesColumnsMap, parameters, timezone, displayTimezone);
        // For period-over-period series, compute and display the actual previous date
        let displaySeriesName = seriesName || '';
        if (seriesOption?.periodOverPeriodMetadata) {
            const { periodOffset, granularity } = seriesOption.periodOverPeriodMetadata;
            // Get the current x-axis value (date)
            const currentDate = param.axisValue ?? param.name;
            if (currentDate) {
                const previousDateStr = computePreviousPeriodDate(currentDate, granularity, periodOffset);
                if (previousDateStr) {
                    // Extract base metric name from dimensions or series name
                    const baseMetricName = seriesOption.dimensions?.[1]?.displayName
                        ?.replace(/\s*\(Previous.*\)$/, '')
                        ?.trim() ||
                        (seriesName || '').replace(/\s*\(Previous.*\)$/, '');
                    displaySeriesName = `${baseMetricName} (${previousDateStr})`;
                }
            }
        }
        const colorIndicator = (0, tooltipStyles_1.formatColorIndicator)(extractColor(marker));
        return (0, tooltipStyles_1.formatCartesianTooltipRow)(colorIndicator, displaySeriesName, (0, tooltipStyles_1.formatTooltipValue)(formattedValue));
    })
        .join('');
    // custom HTML template
    let tooltipHtml = tooltipHtmlTemplate ?? '';
    if (tooltipHtml) {
        tooltipHtml = (0, sanitizeHtml_1.sanitizeHtml)(tooltipHtml);
        const firstParam = params[0];
        const firstValue = firstParam?.value;
        const fields = tooltipHtml.match(/\${(.*?)}/g);
        if (ctx.dataMode === 'tuple' && Array.isArray(firstValue)) {
            // Tuple mode (stacked bars): map dimension names to array indices
            // When stacked, the tuple only contains [categoryValue, numericValue] for each series,
            // so fields not in the current series won't be in dimensionNames.
            // Fall back to looking up the value from the original rows using the x-axis value.
            const xAxisIndex = ctx.flipAxes ? 1 : 0;
            const xAxisValue = firstValue[xAxisIndex];
            // Get all rows matching the x-axis value (there may be multiple due to pivoting)
            const matchingRows = rows && xFieldId
                ? rows.filter((row) => extractCellValue(row[xFieldId]) ===
                    xAxisValue)
                : [];
            fields?.forEach((field) => {
                const ref = field.slice(2, -1);
                const dimensionIndex = firstParam.dimensionNames?.indexOf(ref);
                let val;
                let formatKey = ref;
                if (dimensionIndex !== undefined && dimensionIndex >= 0) {
                    val = unwrapValue(firstValue[dimensionIndex]);
                }
                else {
                    // Fallback: search through matching rows
                    // Try direct lookup first, then translated pivot ref for SQL pivot support
                    const translatedKey = translatePivotRef(ref, pivotValuesColumnsMap);
                    const keysToTry = [ref];
                    if (translatedKey)
                        keysToTry.push(translatedKey);
                    // Also check if ref is a simple metric name that has been pivoted
                    // Also resolves hidden series via hiddenSeriesPivotRefs
                    const pivotColumnFromSeries = (0, exports.findPivotColumnFromSeriesRef)(ref, params, series, pivotValuesColumnsMap, hiddenSeriesPivotRefs);
                    if (pivotColumnFromSeries)
                        keysToTry.push(pivotColumnFromSeries);
                    for (const row of matchingRows) {
                        for (const key of keysToTry) {
                            const cellValue = extractCellValue(row[key]);
                            if (cellValue !== undefined) {
                                val = cellValue;
                                formatKey = key;
                                break;
                            }
                        }
                        if (val !== undefined)
                            break;
                    }
                }
                if (val !== undefined) {
                    const formatted = (0, valueFormatter_1.getFormattedValue)(val, formatKey, itemsMap, undefined, pivotValuesColumnsMap, parameters, timezone, displayTimezone);
                    tooltipHtml = tooltipHtml.replace(field, formatted);
                }
                else {
                    tooltipHtml = tooltipHtml.replace(field, '');
                }
            });
        }
        else if (ctx.dataMode === 'dataset' &&
            firstValue &&
            typeof firstValue === 'object') {
            // Dataset mode: direct property access
            fields?.forEach((field) => {
                const ref = field.slice(2, -1);
                let val = unwrapValue(firstValue[ref]);
                let formatKey = ref;
                // Fallback: try translated pivot ref for SQL pivot support
                if (val === undefined) {
                    const translatedKey = translatePivotRef(ref, pivotValuesColumnsMap);
                    if (translatedKey) {
                        val = unwrapValue(firstValue[translatedKey]);
                        if (val !== undefined) {
                            formatKey = translatedKey;
                        }
                    }
                }
                // Fallback: check if ref is a simple metric name that has been pivoted
                // Also resolves hidden series via hiddenSeriesPivotRefs
                if (val === undefined) {
                    const pivotColumnFromSeries = (0, exports.findPivotColumnFromSeriesRef)(ref, params, series, pivotValuesColumnsMap, hiddenSeriesPivotRefs);
                    if (pivotColumnFromSeries) {
                        val = unwrapValue(firstValue[pivotColumnFromSeries]);
                        formatKey = pivotColumnFromSeries;
                    }
                }
                const formatted = (0, valueFormatter_1.getFormattedValue)(val, formatKey, itemsMap, undefined, pivotValuesColumnsMap, parameters, timezone, displayTimezone);
                tooltipHtml = tooltipHtml.replace(field, formatted);
            });
        }
        else {
            tooltipHtml = '';
        }
    }
    // No data at this x and no custom template (e.g., gap row from
    // continuous-axis padding): suppress the tooltip entirely rather
    // than showing an empty header.
    if (!rowsHtml && !tooltipHtml)
        return '';
    const divider = (0, tooltipStyles_1.getTooltipDivider)();
    const dimensionId = params[0]?.dimensionNames?.[0];
    if (dimensionId !== undefined) {
        const field = itemsMap[dimensionId];
        if ((0, field_1.isTableCalculation)(field)) {
            const headerText = (0, formatting_1.formatItemValue)(field, header, false, parameters);
            return `${(0, tooltipStyles_1.formatTooltipHeader)(headerText)}${divider}${rowsHtml}`;
        }
        const hasFormat = (0, field_1.isField)(field)
            ? field.format !== undefined
            : false;
        if (hasFormat || (0, field_1.isTimeBasedDimension)(field)) {
            // Only re-format when we have a raw value; otherwise trust
            // `header` — re-parsing the already-formatted string breaks
            // tuple-mode time axes (returns "NaT").
            const firstParam = params[0];
            const data = firstParam?.data;
            const valueByDim = data && typeof data === 'object' && !Array.isArray(data)
                ? data[dimensionId]
                : undefined;
            const rawHeaderValue = valueByDim ?? firstParam?.axisValue;
            const headerText = rawHeaderValue !== undefined
                ? (0, valueFormatter_1.getFormattedValue)(rawHeaderValue, dimensionId, itemsMap, undefined, pivotValuesColumnsMap, parameters, timezone, displayTimezone)
                : header;
            return `${(0, tooltipStyles_1.formatTooltipHeader)(headerText)}${divider}${tooltipHtml}${rowsHtml}`;
        }
    }
    return `${(0, tooltipStyles_1.formatTooltipHeader)(header)}${divider}${tooltipHtml}${rowsHtml}`;
};
exports.buildCartesianTooltipFormatter = buildCartesianTooltipFormatter;
//# sourceMappingURL=tooltipFormatter.js.map