"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.OrderByQueryRangeStrategy = void 0;
/**
 * Strategy for filtering partition ranges in ORDER BY query execution context
 * Supports resuming from continuation tokens with proper range-token pair management
 * @hidden
 */
class OrderByQueryRangeStrategy {
    getStrategyType() {
        return "OrderByQuery";
    }
    filterPartitionRanges(targetRanges, continuationRanges, queryInfo) {
        if (!targetRanges ||
            targetRanges.length === 0 ||
            !continuationRanges ||
            continuationRanges.length === 0) {
            return {
                rangeTokenPairs: [],
            };
        }
        if (!queryInfo?.orderByItems ||
            !Array.isArray(queryInfo.orderByItems) ||
            queryInfo.orderByItems.length === 0) {
            throw new Error("Unable to resume ORDER BY query from continuation token. orderByItems is required for ORDER BY queries.");
        }
        const result = {
            rangeTokenPairs: [],
        };
        let filteredRanges = [];
        let resumeRangeFound = false;
        if (continuationRanges && continuationRanges.length > 0) {
            resumeRangeFound = true;
            // Find the range to resume from based on the composite token
            const targetRangeMapping = continuationRanges[continuationRanges.length - 1].range;
            // It is assumed that range mapping array is going to contain only range
            const targetRange = targetRangeMapping;
            const targetContinuationToken = continuationRanges[continuationRanges.length - 1].continuationToken;
            const leftRanges = targetRanges.filter((mapping) => this.isRangeBeforeAnother(mapping.maxExclusive, targetRangeMapping.minInclusive));
            const orderByItems = queryInfo.orderByItems;
            // Create filtering condition for left ranges based on ORDER BY items and sort orders
            const leftFilter = this.createRangeFilterCondition(orderByItems, queryInfo, "left");
            const rightRanges = targetRanges.filter((mapping) => this.isRangeAfterAnother(mapping.minInclusive, targetRangeMapping.maxExclusive));
            // Create filtering condition for right ranges based on ORDER BY items and sort orders
            const rightFilter = this.createRangeFilterCondition(orderByItems, queryInfo, "right");
            // Apply filtering logic for left ranges
            if (leftRanges.length > 0) {
                leftRanges.forEach((range) => {
                    result.rangeTokenPairs.push({
                        range: range,
                        continuationToken: undefined,
                        filteringCondition: leftFilter,
                    });
                });
            }
            result.rangeTokenPairs.push({
                range: targetRange,
                continuationToken: targetContinuationToken,
                filteringCondition: rightFilter,
            });
            // Apply filtering logic for right ranges
            if (rightRanges.length > 0) {
                rightRanges.forEach((range) => {
                    result.rangeTokenPairs.push({
                        range: range,
                        continuationToken: undefined,
                        filteringCondition: rightFilter,
                    });
                });
            }
        }
        // If we couldn't find a specific resume point, include all ranges
        // This can happen with certain types of ORDER BY continuation tokens
        if (!resumeRangeFound) {
            filteredRanges = [...targetRanges];
            filteredRanges.forEach((range) => {
                result.rangeTokenPairs.push({
                    range: range,
                    continuationToken: undefined,
                    filteringCondition: undefined,
                });
            });
        }
        return result;
    }
    /**
     * Creates a filter condition for ranges based on ORDER BY items and sort orders
     * This filter ensures that ranges only return documents based on their position relative to the continuation point
     * @param orderByItems - Array of order by items from the continuation token
     * @param queryInfo - Query information containing sort orders and other metadata
     * @param rangePosition - Whether this is for "left" or "right" ranges relative to continuation point
     * @returns SQL filter condition string for the specified range position
     */
    createRangeFilterCondition(orderByItems, queryInfo, rangePosition) {
        // Extract sort orders from query info
        let sortOrders;
        try {
            sortOrders = this.extractSortOrders(queryInfo);
        }
        catch (error) {
            // If we can't extract sort orders, we cannot create reliable filter conditions
            throw new Error(`Unable to resume ORDER BY query from continuation token. The ORDER BY sort direction configuration ` +
                `in the query plan is invalid or missing. This may indicate a client version mismatch or corrupted continuation token. ` +
                `Please retry the query without a continuation token. Original error: ${error}`);
        }
        // Extract orderByExpressions from nested structure
        let orderByExpressions;
        if (queryInfo &&
            queryInfo.queryInfo &&
            typeof queryInfo.queryInfo === "object" &&
            queryInfo.queryInfo.queryInfo &&
            queryInfo.queryInfo.queryInfo.orderByExpressions &&
            Array.isArray(queryInfo.queryInfo.queryInfo.orderByExpressions)) {
            orderByExpressions = queryInfo.queryInfo.queryInfo.orderByExpressions;
        }
        if (!orderByExpressions || !Array.isArray(orderByExpressions)) {
            throw new Error("Unable to resume ORDER BY query from continuation token. The ORDER BY field configuration " +
                "in the query plan is invalid or missing. This may indicate a client version mismatch or corrupted continuation token. " +
                "Please retry the query without a continuation token.");
        }
        const filterConditions = [];
        // Process each order by item to create filter conditions
        for (let i = 0; i < orderByItems.length && i < sortOrders.length; i++) {
            const orderByItem = orderByItems[i];
            const sortOrder = sortOrders[i];
            if (!orderByItem || orderByItem.item === undefined) {
                continue;
            }
            try {
                // Determine the field path from ORDER BY expressions in query plan
                const fieldPath = this.extractFieldPath(queryInfo, i);
                // Create the comparison condition based on sort order and range position
                const condition = this.createComparisonCondition(fieldPath, orderByItem.item, sortOrder, rangePosition);
                if (condition) {
                    filterConditions.push(condition);
                }
            }
            catch (error) {
                // If we can't extract field path for ORDER BY expressions, we cannot safely resume from continuation token
                // This would lead to incorrect query results, so we must fail the entire request
                throw new Error(`Unable to resume ORDER BY query from continuation token. The ORDER BY field configuration ` +
                    `in the query plan is invalid or incompatible with the continuation token format. ` +
                    `This may indicate a client version mismatch or corrupted continuation token. ` +
                    `Please retry the query without a continuation token. Original error: ${error}`);
            }
        }
        // Combine multiple conditions with AND for multi-field ORDER BY
        const combinedFilter = filterConditions.length > 0 ? `(${filterConditions.join(" AND ")})` : "";
        return combinedFilter;
    }
    /**
     * Extracts sort orders from query info
     * @throws Error if sort order information is missing or invalid
     */
    extractSortOrders(queryInfo) {
        if (!queryInfo) {
            throw new Error("Query information is required to determine ORDER BY sort directions");
        }
        // Extract orderBy from the nested structure: queryInfo.queryInfo.queryInfo.orderBy
        let orderBy;
        if (queryInfo.queryInfo &&
            typeof queryInfo.queryInfo === "object" &&
            queryInfo.queryInfo.queryInfo &&
            typeof queryInfo.queryInfo.queryInfo === "object" &&
            queryInfo.queryInfo.queryInfo.orderBy &&
            Array.isArray(queryInfo.queryInfo.queryInfo.orderBy)) {
            orderBy = queryInfo.queryInfo.queryInfo.orderBy;
        }
        if (!orderBy) {
            throw new Error("ORDER BY sort direction information is missing from query plan");
        }
        return orderBy.map((order, index) => {
            if (typeof order === "string") {
                return order;
            }
            // Handle object format if needed
            if (order && typeof order === "object") {
                const sortOrder = order.direction || order.order || order.sortOrder;
                if (sortOrder) {
                    return sortOrder;
                }
            }
            throw new Error(`ORDER BY sort direction at position ${index + 1} has an invalid format in the query plan`);
        });
    }
    /**
     * Extracts field path from ORDER BY expressions in query plan
     * @throws Error if orderByExpressions are not found or index is out of bounds or expression format is invalid
     */
    extractFieldPath(queryInfo, index) {
        // Try multiple paths to find orderByExpressions due to nested structure
        let orderByExpressions;
        if (queryInfo) {
            // Direct path
            if (queryInfo.orderByExpressions && Array.isArray(queryInfo.orderByExpressions)) {
                orderByExpressions = queryInfo.orderByExpressions;
            }
            // Nested path: queryInfo.queryInfo.queryInfo.orderByExpressions
            else if (queryInfo.queryInfo &&
                typeof queryInfo.queryInfo === "object" &&
                queryInfo.queryInfo.queryInfo &&
                queryInfo.queryInfo.queryInfo.orderByExpressions &&
                Array.isArray(queryInfo.queryInfo.queryInfo.orderByExpressions)) {
                orderByExpressions = queryInfo.queryInfo.queryInfo.orderByExpressions;
            }
        }
        if (!orderByExpressions) {
            throw new Error("ORDER BY field information is missing from query plan");
        }
        if (index >= orderByExpressions.length) {
            throw new Error(`ORDER BY field configuration mismatch: expected at least ${index + 1} fields but found ${orderByExpressions.length}`);
        }
        const expression = orderByExpressions[index];
        // Handle different formats of ORDER BY expressions
        if (typeof expression === "string") {
            // Simple string expression like "c.id" or "_FullTextScore(...)"
            return expression;
        }
        if (expression && typeof expression === "object") {
            // Object format like { expression: "c.id", type: "PropertyRef" }
            if (expression.expression) {
                return expression.expression;
            }
            if (expression.path) {
                return expression.path.replace(/^\//, ""); // Remove leading slash
            }
            if (expression.field) {
                return expression.field;
            }
        }
        throw new Error(`ORDER BY field at position ${index + 1} has an unrecognized format in the query plan`);
    }
    /**
     * Creates a comparison condition based on the field, value, sort order, and range position
     */
    createComparisonCondition(fieldPath, value, sortOrder, rangePosition) {
        const isDescending = sortOrder.toLowerCase() === "descending" || sortOrder.toLowerCase() === "desc";
        // For left ranges (ranges that come before the target):
        // - In ascending order: field > value (left ranges should seek for larger values)
        // - In descending order: field < value (left ranges should seek for smaller values)
        // For right ranges (ranges that come after the target):
        // - In ascending order: field >= value (right ranges have larger values)
        // - In descending order: field <= value (right ranges have smaller values in desc order)
        let operator;
        if (rangePosition === "left") {
            operator = isDescending ? "<" : ">";
        }
        else {
            // right
            operator = isDescending ? "<=" : ">=";
        }
        // Format the value based on its type
        const formattedValue = this.formatValueForSQL(value);
        // Create the condition with proper field reference
        const condition = `${fieldPath} ${operator} ${formattedValue}`;
        return condition;
    }
    /**
     * Formats a value for use in SQL condition
     */
    formatValueForSQL(value) {
        if (value === null || value === undefined) {
            return "null";
        }
        const valueType = typeof value;
        switch (valueType) {
            case "string":
                // Escape single quotes and wrap in quotes
                return `'${value.toString().replace(/'/g, "''")}'`;
            case "number":
            case "bigint":
                return value.toString();
            case "boolean":
                return value ? "true" : "false";
            default:
                // For objects and arrays, convert to JSON string
                if (typeof value === "object") {
                    return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
                }
                return `'${value.toString().replace(/'/g, "''")}'`;
        }
    }
    /**
     * Compares partition key range boundaries with proper handling for inclusive/exclusive semantics
     * @param boundary1 - First boundary to compare
     * @param boundary2 - Second boundary to compare
     * @returns negative if boundary1 is less than boundary2, positive if boundary1 is greater than boundary2, 0 if equal
     */
    comparePartitionKeyBoundaries(boundary1, boundary2) {
        // Handle empty string cases (empty string represents the minimum boundary)
        if (boundary1 === "" && boundary2 === "")
            return 0;
        if (boundary1 === "")
            return -1; // "" < "AA"
        if (boundary2 === "")
            return 1; // "AA" > ""
        // Use standard lexicographic comparison for non-empty boundaries
        return boundary1.localeCompare(boundary2);
    }
    isRangeBeforeAnother(range1MaxExclusive, range2MinInclusive) {
        // Since range1.maxExclusive is NOT part of range1, and range2.minInclusive IS part of range2,
        // range1 comes before range2 if range1.maxExclusive <= range2.minInclusive
        return this.comparePartitionKeyBoundaries(range1MaxExclusive, range2MinInclusive) <= 0;
    }
    isRangeAfterAnother(range1MinInclusive, range2MaxExclusive) {
        // Since range2.maxExclusive is NOT part of range2, and range1.minInclusive IS part of range1,
        // range1 comes after range2 if range1.minInclusive >= range2.maxExclusive
        return this.comparePartitionKeyBoundaries(range1MinInclusive, range2MaxExclusive) >= 0;
    }
}
exports.OrderByQueryRangeStrategy = OrderByQueryRangeStrategy;
//# sourceMappingURL=OrderByQueryRangeStrategy.js.map