import { Operator } from "@limetech/lime-web-components";
import { getNormalizedProperties, getPropertyFromPath, validatePropertyPath, } from "./property-resolution";
/**
 * Dynamic filter values and placeholders that are valid in Lime Query
 */
const VALID_DYNAMIC_VALUES = new Set([
    // Temporal
    '$yesterday',
    '$now',
    '$today',
    '$tomorrow',
    // Ranges
    '$this_week',
    '$this_month',
    '$this_quarter',
    '$this_year',
]);
/**
 * Pattern for relative date functions: $previous_day(x), $next_month(x), etc.
 */
const RELATIVE_DATE_PATTERN = /^\$(previous|next)_(day|week|month|quarter|year)\(\d+\)$/;
/**
 * Pattern for user context variables: $me, $me.office.name, etc.
 */
const USER_CONTEXT_PATTERN = /^\$me(\.\w+)*$/;
/**
 * Check if a value is a valid dynamic value or placeholder
 * @param value
 */
function isValidDynamicValue(value) {
    if (typeof value !== 'string') {
        return false;
    }
    return (VALID_DYNAMIC_VALUES.has(value) ||
        RELATIVE_DATE_PATTERN.test(value) ||
        USER_CONTEXT_PATTERN.test(value));
}
/**
 * Validate a placeholder value
 * @param value The value to check (might be a placeholder)
 * @param activeLimetype The limetype of the active object
 * @param limetypes Record of all available limetypes
 * @returns Validation result with error message if invalid
 */
export function validatePlaceholder(value, activeLimetype, limetypes) {
    // Check if it's a valid dynamic value ($today, $me, etc.)
    if (isValidDynamicValue(value)) {
        return { valid: true };
    }
    // Not a placeholder, no validation needed
    if (typeof value !== 'string' || !value.startsWith('%activeObject%')) {
        return { valid: true };
    }
    // Placeholder used but no active limetype specified
    // This is always valid - activeLimetype is optional and only used for validation
    if (!activeLimetype) {
        return { valid: true };
    }
    // Extract property path from placeholder
    const propertyPath = value.replace(/^%activeObject%\.?/, '');
    // %activeObject% without property path is valid (references the ID)
    if (!propertyPath) {
        return { valid: true };
    }
    // Validate the property path exists on the active limetype
    try {
        const property = getPropertyFromPath(limetypes, activeLimetype, propertyPath);
        if (!property) {
            return {
                valid: false,
                error: `Property path '${propertyPath}' does not exist on limetype '${activeLimetype}'`,
            };
        }
        // Path validation is already done by getPropertyFromPath
        // If we can traverse the path successfully, it means all intermediate
        // properties are valid single relations (belongsTo/hasOne).
        // hasMany relations cannot be traversed in paths.
        return { valid: true };
    }
    catch (error) {
        return {
            valid: false,
            error: `Invalid placeholder path: ${error.message}`,
        };
    }
}
/**
 * Validates a filter expression key (property path).
 * Supports both regular property paths and placeholders.
 *
 * @param key - Property path to validate (e.g., "name", "company.name", "%activeObject%.name")
 * @param limetypes - All limetype definitions
 * @param limetype - The limetype being filtered
 * @param activeLimetype - Active limetype for placeholder resolution
 * @returns Validation result with error message if invalid
 */
export function validateFilterKey(key, limetypes, limetype, activeLimetype) {
    // 1. Handle empty/missing keys
    if (!key) {
        return { valid: false, error: 'Filter key cannot be empty' };
    }
    // 2. Check if key is a placeholder
    if (key.startsWith('%activeObject%')) {
        const placeholderResult = validatePlaceholder(key, activeLimetype, limetypes);
        if (!placeholderResult.valid) {
            return placeholderResult;
        }
        // Extract property path after the placeholder and validate for hasMany/hasAndBelongsToMany
        const propertyPath = key.replace(/^%activeObject%\.?/, '');
        if (propertyPath && activeLimetype) {
            const { error } = validatePropertyPath(limetypes, activeLimetype, propertyPath);
            if (error) {
                return { valid: false, error };
            }
        }
        return placeholderResult;
    }
    // 3. Validate regular property path (including intermediate properties)
    const { error } = validatePropertyPath(limetypes, limetype, key);
    if (error) {
        return { valid: false, error };
    }
    // validatePropertyPath always returns an error if property is undefined,
    // so if we reach here, the property exists and is valid
    return { valid: true };
}
/**
 * Validate a response format against limetype schemas
 * Throws errors for invalid property references
 * Returns visual mode limitations for features not yet supported in visual mode
 * @param responseFormat
 * @param limetypes Record of all available limetypes
 * @param limetype The root limetype for this query
 * @param visualModeEnabled Whether visual mode is enabled
 * @returns Visual mode limitations found (if any)
 */
export function validateResponseFormat(responseFormat, limetypes, limetype, visualModeEnabled = true) {
    const visualModeLimitations = [];
    // Check for visual-mode-unsupported features
    if (visualModeEnabled && responseFormat.aggregates) {
        visualModeLimitations.push('responseFormat.aggregates is not yet supported in visual mode');
    }
    // Validate object properties (throws on invalid properties)
    if (responseFormat.object) {
        const objectLimitations = validatePropertySelection(responseFormat.object, limetypes, limetype, visualModeEnabled);
        visualModeLimitations.push(...objectLimitations);
    }
    return visualModeLimitations;
}
/**
 * Extract non-metadata keys from a property value object
 * Filters out _alias and all # properties (which are treated as comments)
 * @param propValue Property value object
 * @param propName Property name (for error messages)
 * @param visualModeEnabled Whether visual mode is enabled (affects validation)
 * @returns Object with non-metadata keys and any visual mode limitations found
 */
function extractNonMetadataKeys(propValue, propName, visualModeEnabled = true) {
    const keys = Object.keys(propValue);
    const visualModeLimitations = [];
    // Check for # properties other than #description (visual mode limitation)
    if (visualModeEnabled) {
        const unsupportedHashProps = keys.filter((k) => k.startsWith('#') && k !== '#description');
        if (unsupportedHashProps.length > 0) {
            visualModeLimitations.push(`Property '${propName}' contains # properties not supported in visual mode: ${unsupportedHashProps.join(', ')}`);
        }
    }
    // Filter out _alias and all # properties (they are metadata/comments)
    const nonMetadataKeys = keys.filter((k) => k !== '_alias' && !k.startsWith('#'));
    return { keys: nonMetadataKeys, visualModeLimitations };
}
/**
 * Validate a relation property value
 * @param propName Property name
 * @param propValue Property value
 * @param limetypes Record of all available limetypes
 * @param property
 * @param visualModeEnabled Whether visual mode is enabled (affects validation)
 * @returns Visual mode limitations found (if any)
 */
function validateRelationProperty(propName, propValue, limetypes, property, visualModeEnabled = true) {
    // null is valid - just return the relation ID
    if (propValue === null) {
        return [];
    }
    if (typeof propValue !== 'object') {
        throw new TypeError(`Relation property '${propName}' must be null or an object`);
    }
    const propValueObj = propValue;
    const { keys: otherKeys, visualModeLimitations } = extractNonMetadataKeys(propValueObj, propName, visualModeEnabled);
    // If it's just {} or { _alias: "...", "#...": "..." }, that's valid
    if (otherKeys.length === 0) {
        return visualModeLimitations;
    }
    // Otherwise, validate nested properties
    const relatedLimetype = property.relation.getLimetype();
    if (!relatedLimetype) {
        throw new Error(`Could not determine related limetype for property '${propName}'`);
    }
    // Build a clean PropertySelection without metadata for recursive validation
    const cleanSelection = {};
    for (const key of otherKeys) {
        cleanSelection[key] = propValueObj[key];
    }
    const nestedLimitations = validatePropertySelection(cleanSelection, limetypes, relatedLimetype.name, visualModeEnabled);
    return [...visualModeLimitations, ...nestedLimitations];
}
/**
 * Validate a non-relation property value
 * @param propName Property name
 * @param propValue Property value
 * @param visualModeEnabled Whether visual mode is enabled (affects validation)
 * @returns Visual mode limitations found (if any)
 */
function validateNonRelationProperty(propName, propValue, visualModeEnabled = true) {
    // null is valid
    if (propValue === null) {
        return [];
    }
    // Allow empty object {} or object with only metadata (_alias, #...)
    if (typeof propValue === 'object') {
        const { keys: nonMetadataKeys, visualModeLimitations } = extractNonMetadataKeys(propValue, propName, visualModeEnabled);
        if (nonMetadataKeys.length === 0) {
            // {} or { _alias: "...", "#...": "..." } is valid
            return visualModeLimitations;
        }
        throw new Error(`Non-relation property '${propName}' cannot have nested properties other than _alias or # properties (got: ${nonMetadataKeys.join(', ')})`);
    }
    throw new Error(`Non-relation property '${propName}' must be null or an object (got ${typeof propValue})`);
}
/**
 * Validate a single property entry
 * @param propName Property name
 * @param propValue Property value
 * @param normalizedProperties Normalized properties of the limetype
 * @param limetypes Record of all available limetypes
 * @param limetype Current limetype name
 * @param visualModeEnabled Whether visual mode is enabled (affects validation)
 * @returns Visual mode limitations found (if any)
 */
function validateSinglePropertyEntry(propName, propValue, normalizedProperties, limetypes, limetype, visualModeEnabled = true) {
    // Allow empty string (editing state in visual mode)
    if (propName === '') {
        // Only validate empty property name in visual mode
        if (visualModeEnabled && propValue !== null) {
            throw new Error('Empty property name must have null value');
        }
        return [];
    }
    // Validate property exists on limetype
    const property = normalizedProperties[propName];
    if (!property) {
        throw new Error(`Property '${propName}' does not exist on limetype '${limetype}'`);
    }
    // Validate value based on property type
    if (property.relation) {
        return validateRelationProperty(propName, propValue, limetypes, property, visualModeEnabled);
    }
    else {
        return validateNonRelationProperty(propName, propValue, visualModeEnabled);
    }
}
/**
 * Validate a property selection object against a limetype schema
 * Recursively validates nested property selections for relations
 * @param selection
 * @param limetypes Record of all available limetypes
 * @param limetype The limetype for this level of the selection
 * @param visualModeEnabled Whether visual mode is enabled (affects validation)
 * @returns Visual mode limitations found (if any)
 */
export function validatePropertySelection(selection, limetypes, limetype, visualModeEnabled = true) {
    const limetypeObj = limetypes[limetype];
    if (!limetypeObj) {
        throw new Error(`Unknown limetype: ${limetype}`);
    }
    const normalizedProperties = getNormalizedProperties(limetypeObj);
    const allVisualModeLimitations = [];
    for (const [propName, propValue] of Object.entries(selection)) {
        // Skip # properties (comments/metadata that backend accepts via unknown=INCLUDE)
        // Note: _alias only appears inside property value objects, not at this level
        if (propName.startsWith('#')) {
            continue;
        }
        const limitations = validateSinglePropertyEntry(propName, propValue, normalizedProperties, limetypes, limetype, visualModeEnabled);
        allVisualModeLimitations.push(...limitations);
    }
    return allVisualModeLimitations;
}
/**
 * Validate a comparison expression (has 'key' property)
 * @param filter
 * @param activeLimetype
 * @param limetypes
 * @param limetype
 */
function validateComparisonExpression(filter, activeLimetype, limetypes, limetype) {
    // Validate operator
    const allValidOperators = Object.values(Operator);
    if (!allValidOperators.includes(filter.op)) {
        throw new Error(`Unsupported filter operator: ${filter.op}`);
    }
    // Validate filter key
    const keyResult = validateFilterKey(filter.key, limetypes, limetype, activeLimetype);
    if (!keyResult.valid) {
        throw new Error(`Invalid filter key '${filter.key}': ${keyResult.error}`);
    }
    // Validate placeholder
    const result = validatePlaceholder(filter.exp, activeLimetype, limetypes);
    if (!result.valid) {
        throw new Error(`Invalid placeholder in filter '${filter.key}': ${result.error}`);
    }
}
/**
 * Validate a group expression (AND/OR/NOT)
 * @param filter
 * @param activeLimetype
 * @param limetypes
 * @param limetype
 * @param visualModeEnabled
 */
function validateGroupExpression(filter, activeLimetype, limetypes, limetype, visualModeEnabled) {
    // Validate operator
    if (filter.op !== Operator.AND &&
        filter.op !== Operator.OR &&
        filter.op !== Operator.NOT) {
        throw new Error(`Unsupported group operator: ${filter.op}`);
    }
    // Recursively validate children
    if (filter.op === Operator.NOT) {
        validateFilterPlaceholders(filter.exp, activeLimetype, limetypes, limetype, visualModeEnabled);
    }
    else if (filter.op === Operator.AND || filter.op === Operator.OR) {
        const expressions = filter.exp;
        for (const expr of expressions) {
            validateFilterPlaceholders(expr, activeLimetype, limetypes, limetype, visualModeEnabled);
        }
    }
}
/**
 * Validate placeholders in a filter expression
 * @param filter Filter expression to validate
 * @param activeLimetype The limetype of the active object
 * @param limetypes Record of all available limetypes
 * @param limetype The limetype being filtered
 * @param visualModeEnabled Whether visual mode is enabled (affects validation)
 */
function validateFilterPlaceholders(filter, activeLimetype, limetypes, limetype, visualModeEnabled = true) {
    if (!filter) {
        return;
    }
    if ('key' in filter) {
        validateComparisonExpression(filter, activeLimetype, limetypes, limetype);
        return;
    }
    if ('exp' in filter) {
        validateGroupExpression(filter, activeLimetype, limetypes, limetype, visualModeEnabled);
    }
}
/**
 * Validate Lime Query filter and collect errors and visual mode limitations
 * @param filter The filter expression or group to validate
 * @param activeLimetype Optional active object limetype for placeholder validation
 * @param limetypes Record of all available limetypes
 * @param limetype The limetype being filtered
 * @param visualModeEnabled Whether visual mode is enabled
 * @returns Object with validation errors and visual mode limitations
 */
function validateLimeQueryFilterInternal(filter, activeLimetype, limetypes, limetype, visualModeEnabled) {
    const errors = [];
    const limitations = [];
    try {
        validateFilterPlaceholders(filter, activeLimetype, limetypes, limetype, visualModeEnabled);
    }
    catch (error) {
        const errorMessage = error.message;
        // Invalid keys are BOTH spec violations AND rendering limitations:
        // - Backend will reject them (validation error)
        // - Visual mode can't show them in property selector (visual limitation)
        if (errorMessage.includes('Invalid filter key') ||
            errorMessage.includes('Cannot filter on many-relation')) {
            errors.push(`Invalid filter: ${errorMessage}`);
            limitations.push(errorMessage);
        }
        else {
            errors.push(`Invalid filter: ${errorMessage}`);
        }
    }
    return { errors, limitations };
}
/**
 * Validate orderBy specification
 * @param orderBy Array of orderBy items to validate
 * @param limetypes Record of all available limetypes
 * @param limetype The limetype for this Lime Query
 * @returns Array of validation error messages
 */
function validateOrderBy(orderBy, limetypes, limetype) {
    const errors = [];
    if (!Array.isArray(orderBy)) {
        errors.push('orderBy must be an array');
        return errors;
    }
    if (!limetype || !limetypes[limetype]) {
        // Can't validate property paths without limetype
        return errors;
    }
    for (const [index, item] of orderBy.entries()) {
        const itemErrors = validateOrderByItem(item, index, limetypes, limetype);
        errors.push(...itemErrors);
    }
    return errors;
}
/**
 * Validate a single orderBy item
 * @param item The orderBy item to validate
 * @param index The index of the item in the array (for error messages)
 * @param limetypes Record of all available limetypes
 * @param limetype The limetype for this Lime Query
 * @returns Array of validation error messages for this item
 */
function validateOrderByItem(item, index, limetypes, limetype) {
    const errors = [];
    const objectError = validateOrderByItemIsObject(item, index);
    if (objectError) {
        return [objectError];
    }
    const keys = Object.keys(item);
    const keyError = validateOrderByItemHasSingleKey(keys, index);
    if (keyError) {
        return [keyError];
    }
    const propertyPath = keys[0];
    const direction = item[propertyPath];
    const directionError = validateOrderByDirection(direction, index);
    if (directionError) {
        errors.push(directionError);
    }
    const pathError = validateOrderByPropertyPath(propertyPath, limetypes, limetype, index);
    if (pathError) {
        errors.push(pathError);
    }
    return errors;
}
/**
 * Validate that the orderBy item is an object
 * @param item
 * @param index
 */
function validateOrderByItemIsObject(item, index) {
    if (typeof item !== 'object' || item === null) {
        return `orderBy[${index}] must be an object`;
    }
    return null;
}
/**
 * Validate that the orderBy item has exactly one property key
 * @param keys
 * @param index
 */
function validateOrderByItemHasSingleKey(keys, index) {
    if (keys.length === 0) {
        return `orderBy[${index}] must have a property path`;
    }
    if (keys.length > 1) {
        return `orderBy[${index}] must have exactly one property, got ${keys.length}`;
    }
    return null;
}
/**
 * Validate that the sort direction is either ASC or DESC
 * @param direction
 * @param index
 */
function validateOrderByDirection(direction, index) {
    if (direction !== 'ASC' && direction !== 'DESC') {
        return `orderBy[${index}]: direction must be 'ASC' or 'DESC', got '${direction}'`;
    }
    return null;
}
/**
 * Validate that the property path exists on the limetype
 * @param propertyPath
 * @param limetypes
 * @param limetype
 * @param index
 */
function validateOrderByPropertyPath(propertyPath, limetypes, limetype, index) {
    if (!propertyPath || propertyPath === '') {
        return null;
    }
    const property = getPropertyFromPath(limetypes, limetype, propertyPath);
    if (!property) {
        return `orderBy[${index}]: property path '${propertyPath}' does not exist on limetype '${limetype}'`;
    }
    return null;
}
/**
 * Validate Lime Query response format and collect errors/limitations
 * @param responseFormat The response format to validate
 * @param limetypes Record of all available limetypes
 * @param limetype The limetype for this Lime Query
 * @param visualModeEnabled Whether visual mode is enabled
 * @returns Object with validation errors and visual mode limitations
 */
function validateLimeQueryResponseFormatInternal(responseFormat, limetypes, limetype, visualModeEnabled) {
    const errors = [];
    const limitations = [];
    try {
        const formatLimitations = validateResponseFormat(responseFormat, limetypes, limetype, visualModeEnabled);
        limitations.push(...formatLimitations);
    }
    catch (error) {
        errors.push(`Invalid responseFormat: ${error.message}`);
    }
    return { errors, limitations };
}
/**
 * Validate a Lime Query
 * Returns validation result with separate arrays for validity errors and visual mode limitations
 * @param limeQuery The Lime Query to validate
 * @param limetypes Record of all available limetypes
 * @param activeLimetype Optional active object limetype for placeholder validation
 * @param visualModeEnabled Whether visual mode is enabled (affects validation)
 * @returns LimeQueryValidationResult with validity status and any errors/limitations
 */
export function isLimeQuerySupported(limeQuery, limetypes, activeLimetype, visualModeEnabled = true) {
    // Handle empty/undefined Lime Query
    if (!limeQuery) {
        return {
            valid: true,
            visualModeSupported: true,
            validationErrors: [],
            visualModeLimitations: [],
        };
    }
    const validationErrors = [];
    const visualModeLimitations = [];
    // Validate limetype exists
    if (limeQuery.limetype && !limetypes[limeQuery.limetype]) {
        validationErrors.push(`Unknown limetype: ${limeQuery.limetype}`);
    }
    // Check for offset without orderBy (Lime Query requirement)
    if (limeQuery.offset !== undefined && !limeQuery.orderBy) {
        validationErrors.push('offset requires orderBy to be specified');
    }
    // Validate orderBy
    if (limeQuery.orderBy) {
        const orderByErrors = validateOrderBy(limeQuery.orderBy, limetypes, limeQuery.limetype);
        validationErrors.push(...orderByErrors);
    }
    // Check for visual-mode-unsupported top-level properties
    if (visualModeEnabled && limeQuery.offset !== undefined) {
        visualModeLimitations.push('offset is not yet supported in visual mode');
    }
    // Validate filter
    if (limeQuery.filter) {
        const { errors, limitations } = validateLimeQueryFilterInternal(limeQuery.filter, activeLimetype, limetypes, limeQuery.limetype, visualModeEnabled);
        validationErrors.push(...errors);
        visualModeLimitations.push(...limitations);
    }
    // Validate responseFormat
    if (limeQuery.responseFormat) {
        const { errors, limitations } = validateLimeQueryResponseFormatInternal(limeQuery.responseFormat, limetypes, limeQuery.limetype, visualModeEnabled);
        validationErrors.push(...errors);
        visualModeLimitations.push(...limitations);
    }
    return {
        valid: validationErrors.length === 0,
        visualModeSupported: visualModeLimitations.length === 0,
        validationErrors,
        visualModeLimitations,
    };
}
// ============================================================================
// Specialized Validators for Modular Builders
// ============================================================================
/**
 * Validate a ResponseFormat in isolation (for response-format-builder)
 *
 * This validator is designed for the response-format-builder component,
 * which only handles ResponseFormat editing without the full LimeQuery context.
 *
 * @param responseFormat - The response format to validate
 * @param limetypes - Record of all available limetypes
 * @param limetype - The limetype context for validation
 * @param visualModeEnabled - Whether visual mode is enabled (affects validation)
 * @returns Validation result with errors and visual mode limitations
 */
export function validateResponseFormatOnly(responseFormat, limetypes, limetype, visualModeEnabled = true) {
    const validationErrors = [];
    const visualModeLimitations = [];
    // Validate limetype exists
    if (!limetypes[limetype]) {
        validationErrors.push(`Unknown limetype: ${limetype}`);
        // Can't proceed with property validation if limetype is unknown
        return {
            valid: false,
            visualModeSupported: false,
            validationErrors,
            visualModeLimitations,
        };
    }
    // Use internal validation logic
    const { errors, limitations } = validateLimeQueryResponseFormatInternal(responseFormat, limetypes, limetype, visualModeEnabled);
    validationErrors.push(...errors);
    visualModeLimitations.push(...limitations);
    return {
        valid: validationErrors.length === 0,
        visualModeSupported: visualModeLimitations.length === 0,
        validationErrors,
        visualModeLimitations,
    };
}
