/**
 * Schema Sanitizer
 * Cleans and transforms JSON schemas for Gemini/Antigravity API compatibility
 *
 * Uses a multi-phase pipeline matching opencode-antigravity-auth approach:
 * - Phase 1: Convert $refs to description hints
 * - Phase 2a: Merge allOf schemas
 * - Phase 2b: Flatten anyOf/oneOf (select best option)
 * - Phase 2c: Flatten type arrays + update required for nullable
 * - Phase 3: Remove unsupported keywords
 * - Phase 4: Final cleanup (required array validation)
 */

/**
 * Append a hint to a schema's description field.
 * Format: "existing (hint)" or just "hint" if no existing description.
 *
 * @param {Object} schema - Schema object to modify
 * @param {string} hint - Hint text to append
 * @returns {Object} Modified schema with appended description
 */
function appendDescriptionHint(schema, hint) {
    if (!schema || typeof schema !== 'object') return schema;
    const result = { ...schema };
    result.description = result.description
        ? `${result.description} (${hint})`
        : hint;
    return result;
}

/**
 * Score a schema option for anyOf/oneOf selection.
 * Higher scores = more preferred schemas.
 *
 * @param {Object} schema - Schema option to score
 * @returns {number} Score (0-3)
 */
function scoreSchemaOption(schema) {
    if (!schema || typeof schema !== 'object') return 0;

    // Score 3: Object types with properties (most informative)
    if (schema.type === 'object' || schema.properties) return 3;

    // Score 2: Array types with items
    if (schema.type === 'array' || schema.items) return 2;

    // Score 1: Any other non-null type
    if (schema.type && schema.type !== 'null') return 1;

    // Score 0: Null or no type
    return 0;
}

/**
 * Convert $ref references to description hints.
 * Replaces { $ref: "#/$defs/Foo" } with { type: "object", description: "See: Foo" }
 *
 * @param {Object} schema - Schema to process
 * @returns {Object} Schema with refs converted to hints
 */
function convertRefsToHints(schema) {
    if (!schema || typeof schema !== 'object') return schema;
    if (Array.isArray(schema)) return schema.map(convertRefsToHints);

    const result = { ...schema };

    // Handle $ref at this level
    if (result.$ref && typeof result.$ref === 'string') {
        // Extract definition name from ref path (e.g., "#/$defs/Foo" -> "Foo")
        const parts = result.$ref.split('/');
        const defName = parts[parts.length - 1] || 'unknown';
        const hint = `See: ${defName}`;

        // Merge with existing description if present
        const description = result.description
            ? `${result.description} (${hint})`
            : hint;

        // Replace with object type and hint
        return { type: 'object', description };
    }

    // Recursively process properties
    if (result.properties && typeof result.properties === 'object') {
        result.properties = {};
        for (const [key, value] of Object.entries(schema.properties)) {
            result.properties[key] = convertRefsToHints(value);
        }
    }

    // Recursively process items
    if (result.items) {
        if (Array.isArray(result.items)) {
            result.items = result.items.map(convertRefsToHints);
        } else if (typeof result.items === 'object') {
            result.items = convertRefsToHints(result.items);
        }
    }

    // Recursively process anyOf/oneOf/allOf
    for (const key of ['anyOf', 'oneOf', 'allOf']) {
        if (Array.isArray(result[key])) {
            result[key] = result[key].map(convertRefsToHints);
        }
    }

    return result;
}

/**
 * Merge all schemas in an allOf array into a single schema.
 * Properties and required arrays are merged; other fields use first occurrence.
 *
 * @param {Object} schema - Schema with potential allOf to merge
 * @returns {Object} Schema with allOf merged
 */
function mergeAllOf(schema) {
    if (!schema || typeof schema !== 'object') return schema;
    if (Array.isArray(schema)) return schema.map(mergeAllOf);

    let result = { ...schema };

    // Process allOf if present
    if (Array.isArray(result.allOf) && result.allOf.length > 0) {
        const mergedProperties = {};
        const mergedRequired = new Set();
        const otherFields = {};

        for (const subSchema of result.allOf) {
            if (!subSchema || typeof subSchema !== 'object') continue;

            // Merge properties (later overrides earlier)
            if (subSchema.properties) {
                for (const [key, value] of Object.entries(subSchema.properties)) {
                    mergedProperties[key] = value;
                }
            }

            // Union required arrays
            if (Array.isArray(subSchema.required)) {
                for (const req of subSchema.required) {
                    mergedRequired.add(req);
                }
            }

            // Copy other fields (first occurrence wins)
            for (const [key, value] of Object.entries(subSchema)) {
                if (key !== 'properties' && key !== 'required' && !(key in otherFields)) {
                    otherFields[key] = value;
                }
            }
        }

        // Apply merged content
        delete result.allOf;

        // Merge other fields first (parent takes precedence)
        for (const [key, value] of Object.entries(otherFields)) {
            if (!(key in result)) {
                result[key] = value;
            }
        }

        // Merge properties (allOf properties override parent for same keys)
        if (Object.keys(mergedProperties).length > 0) {
            result.properties = { ...mergedProperties, ...(result.properties || {}) };
        }

        // Merge required
        if (mergedRequired.size > 0) {
            const parentRequired = Array.isArray(result.required) ? result.required : [];
            result.required = [...new Set([...mergedRequired, ...parentRequired])];
        }
    }

    // Recursively process properties
    if (result.properties && typeof result.properties === 'object') {
        const newProps = {};
        for (const [key, value] of Object.entries(result.properties)) {
            newProps[key] = mergeAllOf(value);
        }
        result.properties = newProps;
    }

    // Recursively process items
    if (result.items) {
        if (Array.isArray(result.items)) {
            result.items = result.items.map(mergeAllOf);
        } else if (typeof result.items === 'object') {
            result.items = mergeAllOf(result.items);
        }
    }

    return result;
}

/**
 * Flatten anyOf/oneOf by selecting the best option based on scoring.
 * Adds type hints to description when multiple types existed.
 *
 * @param {Object} schema - Schema with potential anyOf/oneOf
 * @returns {Object} Flattened schema
 */
function flattenAnyOfOneOf(schema) {
    if (!schema || typeof schema !== 'object') return schema;
    if (Array.isArray(schema)) return schema.map(flattenAnyOfOneOf);

    let result = { ...schema };

    // Handle anyOf or oneOf
    for (const unionKey of ['anyOf', 'oneOf']) {
        if (Array.isArray(result[unionKey]) && result[unionKey].length > 0) {
            const options = result[unionKey];

            // Collect type names for hint
            const typeNames = [];
            let bestOption = null;
            let bestScore = -1;

            for (const option of options) {
                if (!option || typeof option !== 'object') continue;

                // Collect type name
                const typeName = option.type || (option.properties ? 'object' : null);
                if (typeName && typeName !== 'null') {
                    typeNames.push(typeName);
                }

                // Score and track best option
                const score = scoreSchemaOption(option);
                if (score > bestScore) {
                    bestScore = score;
                    bestOption = option;
                }
            }

            // Remove the union key
            delete result[unionKey];

            // Merge best option into result
            if (bestOption) {
                // Preserve parent description
                const parentDescription = result.description;

                // Recursively flatten the best option
                const flattenedOption = flattenAnyOfOneOf(bestOption);

                // Merge fields from selected option
                for (const [key, value] of Object.entries(flattenedOption)) {
                    if (key === 'description') {
                        // Merge descriptions if different
                        if (value && value !== parentDescription) {
                            result.description = parentDescription
                                ? `${parentDescription} (${value})`
                                : value;
                        }
                    } else if (!(key in result) || key === 'type' || key === 'properties' || key === 'items') {
                        result[key] = value;
                    }
                }

                // Add type hint if multiple types existed
                if (typeNames.length > 1) {
                    const uniqueTypes = [...new Set(typeNames)];
                    result = appendDescriptionHint(result, `Accepts: ${uniqueTypes.join(' | ')}`);
                }
            }
        }
    }

    // Recursively process properties
    if (result.properties && typeof result.properties === 'object') {
        const newProps = {};
        for (const [key, value] of Object.entries(result.properties)) {
            newProps[key] = flattenAnyOfOneOf(value);
        }
        result.properties = newProps;
    }

    // Recursively process items
    if (result.items) {
        if (Array.isArray(result.items)) {
            result.items = result.items.map(flattenAnyOfOneOf);
        } else if (typeof result.items === 'object') {
            result.items = flattenAnyOfOneOf(result.items);
        }
    }

    return result;
}

// ============================================================================
// Enhanced Schema Hints (for preserving semantic information)
// ============================================================================

/**
 * Add hints for enum values (if ≤10 values).
 * This preserves enum information in the description since Gemini
 * may not fully support enums in all cases.
 *
 * @param {Object} schema - Schema to process
 * @returns {Object} Schema with enum hints added to description
 */
function addEnumHints(schema) {
    if (!schema || typeof schema !== 'object') return schema;
    if (Array.isArray(schema)) return schema.map(addEnumHints);

    let result = { ...schema };

    // Add enum hint if present and reasonable size
    if (Array.isArray(result.enum) && result.enum.length > 1 && result.enum.length <= 10) {
        const vals = result.enum.map(v => String(v)).join(', ');
        result = appendDescriptionHint(result, `Allowed: ${vals}`);
    }

    // Recursively process properties
    if (result.properties && typeof result.properties === 'object') {
        const newProps = {};
        for (const [key, value] of Object.entries(result.properties)) {
            newProps[key] = addEnumHints(value);
        }
        result.properties = newProps;
    }

    // Recursively process items
    if (result.items) {
        result.items = Array.isArray(result.items)
            ? result.items.map(addEnumHints)
            : addEnumHints(result.items);
    }

    return result;
}

/**
 * Add hints for additionalProperties: false.
 * This informs the model that extra properties are not allowed.
 *
 * @param {Object} schema - Schema to process
 * @returns {Object} Schema with additionalProperties hints added
 */
function addAdditionalPropertiesHints(schema) {
    if (!schema || typeof schema !== 'object') return schema;
    if (Array.isArray(schema)) return schema.map(addAdditionalPropertiesHints);

    let result = { ...schema };

    if (result.additionalProperties === false) {
        result = appendDescriptionHint(result, 'No extra properties allowed');
    }

    // Recursively process properties
    if (result.properties && typeof result.properties === 'object') {
        const newProps = {};
        for (const [key, value] of Object.entries(result.properties)) {
            newProps[key] = addAdditionalPropertiesHints(value);
        }
        result.properties = newProps;
    }

    // Recursively process items
    if (result.items) {
        result.items = Array.isArray(result.items)
            ? result.items.map(addAdditionalPropertiesHints)
            : addAdditionalPropertiesHints(result.items);
    }

    return result;
}

/**
 * Move unsupported constraints to description hints.
 * This preserves constraint information that would otherwise be lost
 * when we strip unsupported keywords.
 *
 * @param {Object} schema - Schema to process
 * @returns {Object} Schema with constraint hints added to description
 */
function moveConstraintsToDescription(schema) {
    if (!schema || typeof schema !== 'object') return schema;
    if (Array.isArray(schema)) return schema.map(moveConstraintsToDescription);

    const CONSTRAINTS = ['minLength', 'maxLength', 'pattern', 'minimum', 'maximum',
                         'minItems', 'maxItems', 'format'];

    let result = { ...schema };

    for (const constraint of CONSTRAINTS) {
        if (result[constraint] !== undefined && typeof result[constraint] !== 'object') {
            result = appendDescriptionHint(result, `${constraint}: ${result[constraint]}`);
        }
    }

    // Recursively process properties
    if (result.properties && typeof result.properties === 'object') {
        const newProps = {};
        for (const [key, value] of Object.entries(result.properties)) {
            newProps[key] = moveConstraintsToDescription(value);
        }
        result.properties = newProps;
    }

    // Recursively process items
    if (result.items) {
        result.items = Array.isArray(result.items)
            ? result.items.map(moveConstraintsToDescription)
            : moveConstraintsToDescription(result.items);
    }

    return result;
}

/**
 * Flatten array type fields and track nullable properties.
 * Converts { type: ["string", "null"] } to { type: "string" } with nullable hint.
 *
 * @param {Object} schema - Schema to process
 * @param {Set<string>} nullableProps - Set to collect nullable property names (mutated)
 * @param {string} currentPropName - Current property name (for tracking)
 * @returns {Object} Flattened schema
 */
function flattenTypeArrays(schema, nullableProps = null, currentPropName = null) {
    if (!schema || typeof schema !== 'object') return schema;
    if (Array.isArray(schema)) return schema.map(s => flattenTypeArrays(s, nullableProps));

    let result = { ...schema };

    // Handle array type fields
    if (Array.isArray(result.type)) {
        const types = result.type;
        const hasNull = types.includes('null');
        const nonNullTypes = types.filter(t => t !== 'null' && t);

        // Select first non-null type, or 'string' as fallback
        const firstType = nonNullTypes.length > 0 ? nonNullTypes[0] : 'string';
        result.type = firstType;

        // Add hint for multiple types
        if (nonNullTypes.length > 1) {
            result = appendDescriptionHint(result, `Accepts: ${nonNullTypes.join(' | ')}`);
        }

        // Track nullable and add hint
        if (hasNull) {
            result = appendDescriptionHint(result, 'nullable');
            // Track this property as nullable for required array update
            if (nullableProps && currentPropName) {
                nullableProps.add(currentPropName);
            }
        }
    }

    // Recursively process properties, tracking nullable ones
    if (result.properties && typeof result.properties === 'object') {
        const childNullableProps = new Set();
        const newProps = {};

        for (const [key, value] of Object.entries(result.properties)) {
            newProps[key] = flattenTypeArrays(value, childNullableProps, key);
        }
        result.properties = newProps;

        // Remove nullable properties from required array
        if (Array.isArray(result.required) && childNullableProps.size > 0) {
            result.required = result.required.filter(prop => !childNullableProps.has(prop));
            if (result.required.length === 0) {
                delete result.required;
            }
        }
    }

    // Recursively process items
    if (result.items) {
        if (Array.isArray(result.items)) {
            result.items = result.items.map(item => flattenTypeArrays(item, nullableProps));
        } else if (typeof result.items === 'object') {
            result.items = flattenTypeArrays(result.items, nullableProps);
        }
    }

    return result;
}

/**
 * Sanitize JSON Schema for Antigravity API compatibility.
 * Uses allowlist approach - only permit known-safe JSON Schema features.
 * Converts "const" to equivalent "enum" for compatibility.
 * Generates placeholder schema for empty tool schemas.
 */
export function sanitizeSchema(schema) {
    if (!schema || typeof schema !== 'object') {
        // Empty/missing schema - generate placeholder with reason property
        return {
            type: 'object',
            properties: {
                reason: {
                    type: 'string',
                    description: 'Reason for calling this tool'
                }
            },
            required: ['reason']
        };
    }

    // Allowlist of permitted JSON Schema fields
    const ALLOWED_FIELDS = new Set([
        'type',
        'description',
        'properties',
        'required',
        'items',
        'enum',
        'title'
    ]);

    const sanitized = {};

    for (const [key, value] of Object.entries(schema)) {
        // Convert "const" to "enum" for compatibility
        if (key === 'const') {
            sanitized.enum = [value];
            continue;
        }

        // Skip fields not in allowlist
        if (!ALLOWED_FIELDS.has(key)) {
            continue;
        }

        if (key === 'properties' && value && typeof value === 'object') {
            sanitized.properties = {};
            for (const [propKey, propValue] of Object.entries(value)) {
                sanitized.properties[propKey] = sanitizeSchema(propValue);
            }
        } else if (key === 'items' && value && typeof value === 'object') {
            if (Array.isArray(value)) {
                sanitized.items = value.map(item => sanitizeSchema(item));
            } else {
                sanitized.items = sanitizeSchema(value);
            }
        } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
            sanitized[key] = sanitizeSchema(value);
        } else {
            sanitized[key] = value;
        }
    }

    // Ensure we have at least a type
    if (!sanitized.type) {
        sanitized.type = 'object';
    }

    // If object type with no properties, add placeholder
    if (sanitized.type === 'object' && (!sanitized.properties || Object.keys(sanitized.properties).length === 0)) {
        sanitized.properties = {
            reason: {
                type: 'string',
                description: 'Reason for calling this tool'
            }
        };
        sanitized.required = ['reason'];
    }

    return sanitized;
}

/**
 * Convert JSON Schema type names to Google's Protobuf-style uppercase type names.
 * Google's Generative AI API expects uppercase types: STRING, OBJECT, ARRAY, etc.
 *
 * @param {string} type - JSON Schema type name (lowercase)
 * @returns {string} Google-format type name (uppercase)
 */
function toGoogleType(type) {
    if (!type || typeof type !== 'string') return type;
    const typeMap = {
        'string': 'STRING',
        'number': 'NUMBER',
        'integer': 'INTEGER',
        'boolean': 'BOOLEAN',
        'array': 'ARRAY',
        'object': 'OBJECT',
        'null': 'STRING'  // Fallback for null type
    };
    return typeMap[type.toLowerCase()] || type.toUpperCase();
}

/**
 * Cleans JSON schema for Gemini API compatibility.
 * Uses a multi-phase pipeline matching opencode-antigravity-auth approach.
 *
 * @param {Object} schema - The JSON schema to clean
 * @returns {Object} Cleaned schema safe for Gemini API
 */
export function cleanSchema(schema) {
    if (!schema || typeof schema !== 'object') return schema;
    if (Array.isArray(schema)) return schema.map(cleanSchema);

    // Phase 1: Convert $refs to hints
    let result = convertRefsToHints(schema);

    // Phase 1b: Add enum hints (preserves enum info in description)
    result = addEnumHints(result);

    // Phase 1c: Add additionalProperties hints
    result = addAdditionalPropertiesHints(result);

    // Phase 1d: Move constraints to description (before they get stripped)
    result = moveConstraintsToDescription(result);

    // Phase 2a: Merge allOf schemas
    result = mergeAllOf(result);

    // Phase 2b: Flatten anyOf/oneOf
    result = flattenAnyOfOneOf(result);

    // Phase 2c: Flatten type arrays and update required for nullable
    result = flattenTypeArrays(result);

    // Phase 3: Remove unsupported keywords
    const unsupported = [
        'additionalProperties', 'default', '$schema', '$defs',
        'definitions', '$ref', '$id', '$comment', 'title',
        'minLength', 'maxLength', 'pattern', 'format',
        'minItems', 'maxItems', 'examples', 'allOf', 'anyOf', 'oneOf'
    ];

    for (const key of unsupported) {
        delete result[key];
    }

    // Check for unsupported 'format' in string types
    if (result.type === 'string' && result.format) {
        const allowed = ['enum', 'date-time'];
        if (!allowed.includes(result.format)) {
            delete result.format;
        }
    }

    // Phase 4: Final cleanup - recursively clean nested schemas and validate required
    if (result.properties && typeof result.properties === 'object') {
        const newProps = {};
        for (const [key, value] of Object.entries(result.properties)) {
            newProps[key] = cleanSchema(value);
        }
        result.properties = newProps;
    }

    if (result.items) {
        if (Array.isArray(result.items)) {
            result.items = result.items.map(cleanSchema);
        } else if (typeof result.items === 'object') {
            result.items = cleanSchema(result.items);
        }
    }

    // Validate that required array only contains properties that exist
    if (result.required && Array.isArray(result.required) && result.properties) {
        const definedProps = new Set(Object.keys(result.properties));
        result.required = result.required.filter(prop => definedProps.has(prop));
        if (result.required.length === 0) {
            delete result.required;
        }
    }

    // Phase 5: Convert type to Google's uppercase format (STRING, OBJECT, ARRAY, etc.)
    // Only convert at current level - nested types already converted by recursive cleanSchema calls
    if (result.type && typeof result.type === 'string') {
        result.type = toGoogleType(result.type);
    }

    return result;
}
