import { Effect } from 'effect';
import { ErrorCode, MAX_COLS, MAX_ROWS, ValueTag, } from '@bilig/protocol';
import { formatAddress, parseCellAddress } from '@bilig/formula';
import { normalizeRange } from '../../engine-range-utils.js';
import { areCellValuesEqual, cellValueDisplayText, emptyValue, errorValue, normalizePivotLookupText, pivotItemMatches, } from '../../engine-value-utils.js';
import { materializePivotTable } from '../../pivot-engine.js';
import { pivotKey } from '../../workbook-store.js';
import { EnginePivotError } from '../errors.js';
const VISIBLE_PIVOT_SCAN_ROWS = 300;
const VISIBLE_PIVOT_SCAN_COLS = 120;
const VISIBLE_PIVOT_LABEL_COLS = 8;
const VISIBLE_PIVOT_HEADER_ROWS = 24;
function toPivotDefinition(pivot) {
    return {
        groupBy: pivot.groupBy,
        ...(pivot.columnFields ? { columnFields: pivot.columnFields } : {}),
        ...(pivot.filters ? { filters: pivot.filters } : {}),
        ...(pivot.pageFields ? { pageFields: pivot.pageFields } : {}),
        ...(pivot.hiddenItems ? { hiddenItems: pivot.hiddenItems } : {}),
        values: pivot.values,
    };
}
function literalToPivotCellValue(value) {
    if (value === null) {
        return emptyValue();
    }
    if (typeof value === 'number' && Number.isFinite(value)) {
        return { tag: ValueTag.Number, value };
    }
    if (typeof value === 'boolean') {
        return { tag: ValueTag.Boolean, value };
    }
    return { tag: ValueTag.String, value: String(value), stringId: 0 };
}
function normalizeVisiblePivotText(value) {
    return normalizePivotLookupText(value.replace(/\s+/gu, ' '));
}
function addVisiblePivotAlias(aliases, value) {
    const normalized = normalizeVisiblePivotText(value);
    if (normalized.length === 0) {
        return;
    }
    aliases.add(normalized);
    const withoutAggregatePrefix = normalized.replace(/^(SUM|COUNT|AVERAGE|MIN|MAX) OF\s+/u, '');
    if (withoutAggregatePrefix.length > 0) {
        aliases.add(withoutAggregatePrefix);
    }
}
function visiblePivotTextAliases(value) {
    const aliases = new Set();
    addVisiblePivotAlias(aliases, value);
    for (const match of value.matchAll(/\[([^\]]+)\]/gu)) {
        addVisiblePivotAlias(aliases, match[1] ?? '');
    }
    return [...aliases];
}
function visiblePivotCellAliases(value) {
    return visiblePivotTextAliases(cellValueDisplayText(value));
}
function pivotCacheFieldName(value, index) {
    const label = cellValueDisplayText(value).trim();
    return label.length > 0 ? label : `Column ${String(index + 1)}`;
}
function cellValueToPivotLiteral(value) {
    switch (value.tag) {
        case ValueTag.Empty:
            return null;
        case ValueTag.Number:
            return value.value;
        case ValueTag.Boolean:
            return value.value;
        case ValueTag.String:
            return value.value;
        case ValueTag.Error:
            return value.code;
    }
}
function emptyPivotLookupAggregateState() {
    return {
        sum: 0,
        count: 0,
        numericCount: 0,
        min: Number.POSITIVE_INFINITY,
        max: Number.NEGATIVE_INFINITY,
        product: 1,
    };
}
function accumulatePivotLookupValue(state, value) {
    if (value.tag !== ValueTag.Empty) {
        state.count += 1;
    }
    if (value.tag !== ValueTag.Number) {
        return;
    }
    state.sum += value.value;
    state.numericCount += 1;
    state.min = Math.min(state.min, value.value);
    state.max = Math.max(state.max, value.value);
    state.product *= value.value;
}
function finalizePivotLookupAggregate(mode, state) {
    switch (mode) {
        case 'sum':
            return state.sum;
        case 'count':
            return state.count;
        case 'countNums':
            return state.numericCount;
        case 'average':
            return state.numericCount === 0 ? 0 : state.sum / state.numericCount;
        case 'min':
            return state.numericCount === 0 ? 0 : state.min;
        case 'max':
            return state.numericCount === 0 ? 0 : state.max;
        case 'product':
            return state.numericCount === 0 ? 0 : state.product;
    }
}
function refreshedSourcePivotCache(sourceRows) {
    const headerRow = sourceRows[0];
    if (!headerRow || headerRow.length === 0) {
        return undefined;
    }
    const cacheFields = headerRow.map(pivotCacheFieldName);
    const cachedRecords = sourceRows
        .slice(1)
        .map((row) => cacheFields.map((_, columnIndex) => cellValueToPivotLiteral(row[columnIndex] ?? emptyValue())));
    return { cacheFields, cachedRecords };
}
function pivotCacheMatches(pivot, cache) {
    return (cache !== undefined &&
        JSON.stringify(pivot.cacheFields ?? []) === JSON.stringify(cache.cacheFields) &&
        JSON.stringify(pivot.cachedRecords ?? []) === JSON.stringify(cache.cachedRecords));
}
function visiblePivotFilterItemAliases(value) {
    const display = cellValueDisplayText(value);
    const bracketValues = [...display.matchAll(/\[([^\]]+)\]/gu)].map((match) => match[1] ?? '').filter((part) => part.trim().length > 0);
    if (bracketValues.length === 0) {
        return visiblePivotTextAliases(display);
    }
    const aliases = new Set();
    addVisiblePivotAlias(aliases, display);
    addVisiblePivotAlias(aliases, bracketValues[bracketValues.length - 1]);
    return [...aliases];
}
function visiblePivotCellMatches(cell, expectedAliases) {
    return visiblePivotCellAliases(cell).some((alias) => expectedAliases.has(alias));
}
function findVisiblePivotMeasureRow(sheetName, startRow, startCol, dataField, readCell) {
    const expectedAliases = new Set(visiblePivotTextAliases(dataField));
    if (expectedAliases.size === 0) {
        return undefined;
    }
    const endRow = Math.min(MAX_ROWS - 1, startRow + VISIBLE_PIVOT_SCAN_ROWS);
    const endCol = Math.min(MAX_COLS - 1, startCol + VISIBLE_PIVOT_LABEL_COLS);
    for (let row = startRow; row <= endRow; row += 1) {
        for (let col = startCol; col <= endCol; col += 1) {
            if (visiblePivotCellMatches(readCell(sheetName, row, col), expectedAliases)) {
                return row;
            }
        }
    }
    return undefined;
}
function visiblePivotHeaderRowsForFilter(sheetName, startRow, startCol, valueRow, filterField, readCell) {
    const fieldAliases = new Set(visiblePivotTextAliases(filterField));
    const rows = new Set();
    const endRow = Math.min(valueRow, startRow + VISIBLE_PIVOT_HEADER_ROWS);
    const endCol = Math.min(MAX_COLS - 1, startCol + VISIBLE_PIVOT_SCAN_COLS);
    for (let row = startRow; row <= endRow; row += 1) {
        for (let col = startCol; col <= endCol; col += 1) {
            if (!visiblePivotCellMatches(readCell(sheetName, row, col), fieldAliases)) {
                continue;
            }
            rows.add(row);
            if (row + 1 <= valueRow) {
                rows.add(row + 1);
            }
            if (row + 2 <= valueRow) {
                rows.add(row + 2);
            }
        }
    }
    if (rows.size === 0) {
        for (let row = startRow; row <= endRow; row += 1) {
            rows.add(row);
        }
    }
    return [...rows].toSorted((left, right) => left - right);
}
function findVisiblePivotFilterColumns(sheetName, startRow, startCol, valueRow, filter, readCell) {
    const itemAliases = new Set(visiblePivotFilterItemAliases(filter.item));
    if (itemAliases.size === 0) {
        return [];
    }
    const rows = visiblePivotHeaderRowsForFilter(sheetName, startRow, startCol, valueRow, filter.field, readCell);
    const endCol = Math.min(MAX_COLS - 1, startCol + VISIBLE_PIVOT_SCAN_COLS);
    const columns = new Set();
    for (const row of rows) {
        for (let col = startCol + 1; col <= endCol; col += 1) {
            if (visiblePivotCellMatches(readCell(sheetName, row, col), itemAliases)) {
                columns.add(col);
            }
        }
    }
    return [...columns].toSorted((left, right) => left - right);
}
function firstVisiblePivotValueColumn(sheetName, startCol, valueRow, readCell) {
    const endCol = Math.min(MAX_COLS - 1, startCol + VISIBLE_PIVOT_SCAN_COLS);
    for (let col = startCol + 1; col <= endCol; col += 1) {
        if (readCell(sheetName, valueRow, col).tag !== ValueTag.Empty) {
            return col;
        }
    }
    return undefined;
}
function findVisiblePivotValueColumn(sheetName, startRow, startCol, valueRow, filters, readCell) {
    if (filters.length === 0) {
        return firstVisiblePivotValueColumn(sheetName, startCol, valueRow, readCell);
    }
    let candidates;
    for (const filter of filters) {
        const columns = findVisiblePivotFilterColumns(sheetName, startRow, startCol, valueRow, filter, readCell);
        if (columns.length === 0) {
            return undefined;
        }
        if (!candidates) {
            candidates = new Set(columns);
            continue;
        }
        candidates = new Set(columns.filter((column) => candidates?.has(column)));
        if (candidates.size === 0) {
            return undefined;
        }
    }
    return candidates ? Math.min(...candidates) : undefined;
}
export function createEnginePivotService(args) {
    const clearPivotOutputCell = (cellIndex) => {
        const currentFlags = args.state.workbook.cellStore.flags[cellIndex] ?? 0;
        const currentValue = args.state.workbook.cellStore.getValue(cellIndex, (id) => args.state.strings.get(id));
        if (currentValue.tag === ValueTag.Empty && (currentFlags & 128 /* CellFlags.PivotOutput */) === 0) {
            args.state.pivotOutputOwners.delete(cellIndex);
            return false;
        }
        args.state.pivotOutputOwners.delete(cellIndex);
        args.state.workbook.cellStore.setValue(cellIndex, emptyValue());
        args.state.workbook.cellStore.flags[cellIndex] =
            currentFlags & ~(2 /* CellFlags.HasFormula */ | 4 /* CellFlags.JsOnly */ | 8 /* CellFlags.InCycle */ | 64 /* CellFlags.SpillChild */ | 128 /* CellFlags.PivotOutput */);
        args.state.workbook.pruneCellIfEmpty(cellIndex);
        return true;
    };
    const setPivotOutputCellValue = (cellIndex, value, ownerKey) => {
        const currentFlags = args.state.workbook.cellStore.flags[cellIndex] ?? 0;
        const currentValue = args.state.workbook.cellStore.getValue(cellIndex, (id) => args.state.strings.get(id));
        const nextFlags = (currentFlags & ~(2 /* CellFlags.HasFormula */ | 4 /* CellFlags.JsOnly */ | 8 /* CellFlags.InCycle */ | 64 /* CellFlags.SpillChild */)) | 128 /* CellFlags.PivotOutput */;
        if (areCellValuesEqual(currentValue, value) && currentFlags === nextFlags && args.state.pivotOutputOwners.get(cellIndex) === ownerKey) {
            return false;
        }
        args.state.workbook.cellStore.setValue(cellIndex, value, value.tag === ValueTag.String ? args.state.strings.intern(value.value) : 0);
        args.state.workbook.cellStore.flags[cellIndex] = nextFlags;
        args.state.pivotOutputOwners.set(cellIndex, ownerKey);
        return true;
    };
    const clearOwnedPivotNow = (pivot) => {
        const changedCellIndices = [];
        const ownerKey = pivotKey(pivot.sheetName, pivot.address);
        const owner = parseCellAddress(pivot.address, pivot.sheetName);
        for (let rowOffset = 0; rowOffset < pivot.rows; rowOffset += 1) {
            for (let colOffset = 0; colOffset < pivot.cols; colOffset += 1) {
                const cellIndex = args.state.workbook.getCellIndex(pivot.sheetName, formatAddress(owner.row + rowOffset, owner.col + colOffset));
                if (cellIndex === undefined || args.state.pivotOutputOwners.get(cellIndex) !== ownerKey) {
                    continue;
                }
                if (clearPivotOutputCell(cellIndex)) {
                    changedCellIndices.push(cellIndex);
                }
            }
        }
        return changedCellIndices;
    };
    const writePivotOutput = (pivot, rows, cols, values, changedCellIndices) => {
        const sheet = args.state.workbook.getOrCreateSheet(pivot.sheetName);
        const owner = parseCellAddress(pivot.address, pivot.sheetName);
        const ownerKey = pivotKey(pivot.sheetName, pivot.address);
        const changedSeen = new Set(changedCellIndices);
        for (let rowOffset = 0; rowOffset < rows; rowOffset += 1) {
            for (let colOffset = 0; colOffset < cols; colOffset += 1) {
                const valueIndex = rowOffset * cols + colOffset;
                const cellValue = values[valueIndex] ?? emptyValue();
                const cellIndex = args.ensureCellTrackedByCoords(sheet.id, owner.row + rowOffset, owner.col + colOffset);
                if (setPivotOutputCellValue(cellIndex, cellValue, ownerKey)) {
                    if (!changedSeen.has(cellIndex)) {
                        changedSeen.add(cellIndex);
                        changedCellIndices.push(cellIndex);
                    }
                }
            }
        }
        if (pivot.rows !== rows || pivot.cols !== cols) {
            args.state.workbook.setPivot({ ...pivot, rows, cols });
        }
        return changedCellIndices;
    };
    const isPivotOutputBlocked = (pivot, startRow, startCol, rows, cols) => {
        const ownerKey = pivotKey(pivot.sheetName, pivot.address);
        for (let rowOffset = 0; rowOffset < rows; rowOffset += 1) {
            for (let colOffset = 0; colOffset < cols; colOffset += 1) {
                const targetIndex = args.state.workbook.getCellIndex(pivot.sheetName, formatAddress(startRow + rowOffset, startCol + colOffset));
                if (targetIndex === undefined) {
                    continue;
                }
                const pivotOwner = args.state.pivotOutputOwners.get(targetIndex);
                if (pivotOwner && pivotOwner !== ownerKey) {
                    return true;
                }
                if (args.state.formulas.get(targetIndex)) {
                    return true;
                }
                const targetFlags = args.state.workbook.cellStore.flags[targetIndex] ?? 0;
                if ((targetFlags & 64 /* CellFlags.SpillChild */) !== 0) {
                    return true;
                }
                const targetValue = args.state.workbook.cellStore.getValue(targetIndex, (id) => args.state.strings.get(id));
                if (!pivotOwner && targetValue.tag !== ValueTag.Empty) {
                    return true;
                }
            }
        }
        return false;
    };
    const guardPivotOutputWrite = (pivot, startRow, startCol, rows, cols, changedCellIndices) => {
        if (startRow + rows > MAX_ROWS || startCol + cols > MAX_COLS) {
            return writePivotOutput(pivot, 1, 1, [errorValue(ErrorCode.Spill)], changedCellIndices);
        }
        if (isPivotOutputBlocked(pivot, startRow, startCol, rows, cols)) {
            return writePivotOutput(pivot, 1, 1, [errorValue(ErrorCode.Blocked)], changedCellIndices);
        }
        return undefined;
    };
    const readPivotSourceRows = (range) => {
        const bounds = normalizeRange(range);
        const rows = [];
        for (let row = bounds.startRow; row <= bounds.endRow; row += 1) {
            const values = [];
            for (let col = bounds.startCol; col <= bounds.endCol; col += 1) {
                const cellIndex = args.state.workbook.getCellIndex(range.sheetName, formatAddress(row, col));
                values.push(cellIndex === undefined ? emptyValue() : args.state.workbook.cellStore.getValue(cellIndex, (id) => args.state.strings.get(id)));
            }
            rows.push(values);
        }
        return rows;
    };
    const readPivotCachedRows = (pivot) => {
        if (!pivot.cacheFields || !pivot.cachedRecords) {
            return null;
        }
        return [
            pivot.cacheFields.map((field) => ({ tag: ValueTag.String, value: field, stringId: 0 })),
            ...pivot.cachedRecords.map((row) => row.map(literalToPivotCellValue)),
        ];
    };
    const readPivotRows = (pivot) => {
        if (pivot.source && !pivot.cacheOnly && args.state.workbook.getSheet(pivot.source.sheetName)) {
            return { rows: readPivotSourceRows(pivot.source), provenance: 'source' };
        }
        const cachedRows = readPivotCachedRows(pivot);
        if (cachedRows) {
            return { rows: cachedRows, provenance: 'cache' };
        }
        return pivot.source && args.state.workbook.getSheet(pivot.source.sheetName)
            ? { rows: readPivotSourceRows(pivot.source), provenance: 'source' }
            : null;
    };
    const refreshPivotCacheFromSourceRows = (pivot, sourceRows) => {
        if (!pivot.source || pivot.cacheOnly) {
            return pivot;
        }
        const cache = refreshedSourcePivotCache(sourceRows);
        if (!cache || pivotCacheMatches(pivot, cache)) {
            return pivot;
        }
        return args.state.workbook.setPivot({
            ...pivot,
            cacheFields: cache.cacheFields,
            cachedRecords: cache.cachedRecords.map((row) => row.slice()),
        });
    };
    const readVisibleCell = (sheetName, row, col) => {
        const cellIndex = args.state.workbook.getCellIndex(sheetName, formatAddress(row, col));
        return cellIndex === undefined ? emptyValue() : args.state.workbook.cellStore.getValue(cellIndex, (id) => args.state.strings.get(id));
    };
    const materializePivotNow = (pivot) => {
        const changedCellIndices = clearOwnedPivotNow(pivot);
        const sourceRows = readPivotRows(pivot);
        if (!sourceRows) {
            return writePivotOutput(pivot, 1, 1, [errorValue(ErrorCode.Ref)], changedCellIndices);
        }
        const activePivot = sourceRows.provenance === 'source' ? refreshPivotCacheFromSourceRows(pivot, sourceRows.rows) : pivot;
        const materialized = materializePivotTable(toPivotDefinition(activePivot), sourceRows.rows);
        if (materialized.kind === 'error') {
            return writePivotOutput(activePivot, materialized.rows, materialized.cols, materialized.values, changedCellIndices);
        }
        const owner = parseCellAddress(activePivot.address, activePivot.sheetName);
        const blockedOutput = guardPivotOutputWrite(activePivot, owner.row, owner.col, materialized.rows, materialized.cols, changedCellIndices);
        if (blockedOutput) {
            return blockedOutput;
        }
        return writePivotOutput(activePivot, materialized.rows, materialized.cols, materialized.values, changedCellIndices);
    };
    const resolveVisiblePivotDataNow = (sheetName, address, dataField, filters) => {
        if (!args.state.workbook.getSheet(sheetName)) {
            return undefined;
        }
        const anchor = parseCellAddress(address, sheetName);
        const valueRow = findVisiblePivotMeasureRow(sheetName, anchor.row, anchor.col, dataField, readVisibleCell);
        if (valueRow === undefined) {
            return undefined;
        }
        const valueCol = findVisiblePivotValueColumn(sheetName, anchor.row, anchor.col, valueRow, filters, readVisibleCell);
        if (valueCol === undefined) {
            return undefined;
        }
        return readVisibleCell(sheetName, valueRow, valueCol);
    };
    const resolvePivotDataNow = (sheetName, address, dataField, filters) => {
        const visibleFallback = () => resolveVisiblePivotDataNow(sheetName, address, dataField, filters) ?? errorValue(ErrorCode.Ref);
        const target = parseCellAddress(address, sheetName);
        const pivot = args.state.workbook.listPivots().find((candidate) => {
            if (candidate.sheetName !== sheetName || candidate.rows <= 0 || candidate.cols <= 0) {
                return false;
            }
            const owner = parseCellAddress(candidate.address, candidate.sheetName);
            return (target.row >= owner.row &&
                target.row < owner.row + candidate.rows &&
                target.col >= owner.col &&
                target.col < owner.col + candidate.cols);
        });
        if (!pivot) {
            return visibleFallback();
        }
        const normalizedDataField = normalizePivotLookupText(dataField);
        const valueField = pivot.values.find((field) => {
            const defaultLabel = `${field.summarizeBy.toUpperCase()} of ${field.sourceColumn}`;
            return (normalizePivotLookupText(field.sourceColumn) === normalizedDataField ||
                normalizePivotLookupText(field.outputLabel?.trim() ?? '') === normalizedDataField ||
                normalizePivotLookupText(defaultLabel) === normalizedDataField);
        });
        if (!valueField) {
            return visibleFallback();
        }
        const pivotRows = readPivotRows(pivot);
        if (!pivotRows) {
            return visibleFallback();
        }
        const sourceRows = pivotRows.rows;
        const headerRow = sourceRows[0];
        if (!headerRow || headerRow.length === 0) {
            return visibleFallback();
        }
        const headerLookup = new Map();
        headerRow.forEach((cell, index) => {
            const normalized = normalizePivotLookupText(cellValueDisplayText(cell));
            if (normalized.length > 0 && !headerLookup.has(normalized)) {
                headerLookup.set(normalized, index);
            }
        });
        const valueColumnIndex = headerLookup.get(normalizePivotLookupText(valueField.sourceColumn));
        if (valueColumnIndex === undefined) {
            return visibleFallback();
        }
        const materializedFilters = filters.map((filter) => ({
            fieldIndex: headerLookup.get(normalizePivotLookupText(filter.field)),
            item: filter.item,
        }));
        if (materializedFilters.some((filter) => filter.fieldIndex === undefined)) {
            return visibleFallback();
        }
        for (let filterIndex = 0; filterIndex < materializedFilters.length; filterIndex += 1) {
            const filter = materializedFilters[filterIndex];
            const fieldIndex = filter.fieldIndex;
            const itemSeen = sourceRows.slice(1).some((row) => pivotItemMatches(row[fieldIndex] ?? emptyValue(), filter.item));
            if (!itemSeen) {
                return visibleFallback();
            }
        }
        let matched = filters.length === 0;
        const aggregate = emptyPivotLookupAggregateState();
        for (let rowIndex = 1; rowIndex < sourceRows.length; rowIndex += 1) {
            const row = sourceRows[rowIndex] ?? [];
            const matches = materializedFilters.every((filter) => pivotItemMatches(row[filter.fieldIndex] ?? emptyValue(), filter.item));
            if (!matches) {
                continue;
            }
            matched = true;
            const value = row[valueColumnIndex] ?? emptyValue();
            accumulatePivotLookupValue(aggregate, value);
        }
        return matched ? { tag: ValueTag.Number, value: finalizePivotLookupAggregate(valueField.summarizeBy, aggregate) } : visibleFallback();
    };
    const clearPivotForCellNow = (cellIndex) => {
        const ownerKey = args.state.pivotOutputOwners.get(cellIndex);
        if (!ownerKey) {
            return [];
        }
        const pivot = args.state.workbook.getPivotByKey(ownerKey);
        if (!pivot) {
            args.state.pivotOutputOwners.delete(cellIndex);
            return [];
        }
        return args.applyDerivedOp({
            kind: 'deletePivotTable',
            sheetName: pivot.sheetName,
            address: pivot.address,
        });
    };
    return {
        materializePivot(pivot) {
            return Effect.try({
                try: () => materializePivotNow(pivot),
                catch: (cause) => new EnginePivotError({
                    message: `Failed to materialize pivot ${pivot.name}`,
                    cause,
                }),
            });
        },
        resolvePivotData(sheetName, address, dataField, filters) {
            return Effect.try({
                try: () => resolvePivotDataNow(sheetName, address, dataField, filters),
                catch: (cause) => new EnginePivotError({
                    message: `Failed to resolve pivot data for ${sheetName}!${address}`,
                    cause,
                }),
            });
        },
        clearOwnedPivot(pivot) {
            return Effect.try({
                try: () => clearOwnedPivotNow(pivot),
                catch: (cause) => new EnginePivotError({
                    message: `Failed to clear pivot output ownership for ${pivot.name}`,
                    cause,
                }),
            });
        },
        clearPivotForCell(cellIndex) {
            return Effect.try({
                try: () => clearPivotForCellNow(cellIndex),
                catch: (cause) => new EnginePivotError({
                    message: `Failed to clear pivot ownership for cell ${cellIndex}`,
                    cause,
                }),
            });
        },
        materializePivotNow,
        resolvePivotDataNow,
        clearOwnedPivotNow,
        clearPivotForCellNow,
    };
}
//# sourceMappingURL=pivot-service.js.map