import { SchemaValidationError } from '@takeshape/errors';
import { createAjv } from '@takeshape/json-schema';
import { DEFAULT_ENTITLEMENTS, DEFAULT_MIN_SCHEDULE_TRIGGER_INTERVAL, ensureArray, isIntegerLike, isRecord, value } from '@takeshape/util';
import { JSONPath } from 'jsonpath-plus';
import flatten from 'lodash/flatten.js';
import get from 'lodash/get.js';
import initial from 'lodash/initial.js';
import isEqual from 'lodash/isEqual.js';
import isUndefined from 'lodash/isUndefined.js';
import last from 'lodash/last.js';
import pick from 'lodash/pick.js';
import size from 'lodash/size.js';
import uniqBy from 'lodash/uniqBy.js';
import pMap from 'p-map';
import coerce from 'semver/functions/coerce.js';
import gte from 'semver/functions/gte.js';
import lt from 'semver/functions/lt.js';
import neq from 'semver/functions/neq.js';
import { GUARDS_SCHEMA_PATH, getGuards, isGuardEnabled } from "../agents.js";
import { getBuiltInShapes } from "../builtin-schema.js";
import { isEnumLikeSchema } from "../enum.js";
import { isLatestProjectSchemaJSON } from "../project-schema/index.js";
import { createGetNamespace, getRefShapeName, getToolRef, isValidServiceId, parsePropertyRef, parseRef, propertyRefItemToPath, propertyRefItemToResolverPath, refItemToNamespacedShapeName, refItemToShape, serializeRef } from "../refs.js";
import { getRelationship } from "../relationships.js";
import { scalars } from "../scalars.js";
import { getAllRefs } from "../schema-util.js";
import authSchemas from '../schemas/auth-schemas.json' with { type: 'json' };
import { allProjectSchemas } from "../schemas/index.js";
import { isValidTemplate } from "../template-shapes/index.js";
import { legacyProjectSchemaImportOptionalProps, projectSchemaImportOptionalProps } from "../types/types.js";
import { isAIResolver, isBasicResolver, isComposeResolver, isExtendsSchema, isObjectSchema } from "../types/utils.js";
import { isUnionSchema } from "../unions.js";
import { getMcpToolMap, TOOL_SCHEMA_PATH } from '../util/mcp.js';
import { findSchemaAtQueryFieldPath, getQueryFieldPath } from "../util/query-field-path.js";
import { getShape } from "../util/shapes.js";
import { CURRENT_SCHEMA_VERSION, LEGACY_API_VERSION, LEGACY_SCHEMA_VERSION } from "../versions.js";
import { defaultWorkflow } from "../workflows.js";
import { validateAgents, validateAIToolConfig } from "./ai.js";
import { formatError } from "./errors.js";
import { pushErrors } from './util.js';
function findDuplicates(items) {
    const seen = {};
    for (const item of items) {
        if (seen[item.value]) {
            seen[item.value].push(item.path);
        }
        else {
            seen[item.value] = [item.path];
        }
    }
    return Object.entries(seen)
        .filter((entry) => entry[1].length > 1)
        .flatMap((entry) => ({ value: entry[0], paths: entry[1] }));
}
function checkShapeIds(shapes) {
    const items = Object.entries(shapes).map((entry) => ({
        path: ['shapes', entry[0], 'id'],
        value: entry[1].id
    }));
    return findDuplicates(items).flatMap((dupe) => dupe.paths.map((path) => ({
        path,
        type: 'conflict',
        message: `Shapes must have unique ids. "${dupe.value}" is not unique.`
    })));
}
function checkShapeNames(shapes) {
    Object.entries(shapes).map((entry) => ({
        path: ['shapes', entry[0], 'id'],
        value: entry[1].id
    }));
    return Object.entries(shapes)
        .filter(([name, shape]) => name !== shape.name)
        .map(([name, shape]) => ({
        path: ['shapes', name, 'name'],
        type: 'conflict',
        message: `Shape.name "${shape.name}" must match key "${name}".`
    }));
}
function checkToolNames(tools) {
    return Object.entries(tools)
        .filter(([name, tool]) => name !== tool.name)
        .map(([name, tool]) => ({
        path: [...TOOL_SCHEMA_PATH, name, 'name'],
        type: 'conflict',
        message: `Tool.name "${tool.name}" must match key "${name}".`
    }));
}
function checkWorkflowStepNames(workflows) {
    return Object.entries(workflows).flatMap(([name, workflow]) => {
        const items = workflow.steps.map((step, i) => ({
            path: ['workflows', name, 'steps', i, 'name'],
            value: step.name.toLowerCase()
        }));
        return findDuplicates(items).flatMap((dupe) => dupe.paths.map((path) => ({
            path,
            type: 'conflict',
            message: `Workflow steps must have unique names. "${dupe.value}" is not unique in workflow "${workflow.name}."`
        })));
    });
}
function checkWorkflowStepKeys(workflows) {
    const items = Object.entries(workflows).flatMap(([name, workflow]) => workflow.steps.map((step, i) => ({
        path: ['workflows', name, 'steps', i, 'key'],
        value: step.key
    })));
    return findDuplicates(items).flatMap((dupe) => dupe.paths.map((path) => ({
        path,
        type: 'conflict',
        message: `Workflow keys must be unique at the schema level. Key "${dupe.value}" is not unique.`
    })));
}
function validateWorkflowVersion(projectSchema) {
    const errors = [];
    if (projectSchema.apiVersion === LEGACY_API_VERSION && Object.keys(projectSchema.workflows).length) {
        errors.push({
            path: ['workflows'],
            type: 'invalidVersion',
            message: `apiVersion ${projectSchema.apiVersion} does not support workflows`
        });
    }
    return errors;
}
function validateWorkflows(projectSchema) {
    const errors = validateWorkflowVersion(projectSchema);
    for (const name of Object.keys(projectSchema.shapes)) {
        const shape = projectSchema.shapes[name];
        if (shape.workflow && shape.workflow !== defaultWorkflow.name && !projectSchema.workflows[shape.workflow]) {
            errors.push({
                type: 'notFound',
                path: ['shapes', name, 'workflow'],
                message: `Invalid Workflow "${shape.workflow}" for shape "${shape.name}"`
            });
        }
    }
    return errors;
}
function getSemver(schemaVersion) {
    return coerce(schemaVersion) ?? LEGACY_SCHEMA_VERSION;
}
function enumerateBasicResolvers(resolver, path) {
    const results = [];
    const visit = (resolver, path) => {
        if (isComposeResolver(resolver)) {
            resolver.compose.forEach((resolver, i) => {
                visit(resolver, [...path, 'compose', i]);
            });
        }
        else {
            results.push([resolver, path]);
        }
    };
    visit(resolver, path);
    return results;
}
function validateResolver(context, projectSchema, basePath, baseResolver) {
    const errors = [];
    const isValidShapeName = (shapeName) => {
        if (!shapeName) {
            return false;
        }
        const shape = getShape(projectSchema, shapeName) ?? getShape({ shapes: context.builtInShapes }, shapeName);
        if (shape?.model) {
            return true;
        }
        return Boolean(shape?.cache?.enabled && shape.loaders);
    };
    const validateDeletgateResolver = (resolver, path) => {
        const parsed = parsePropertyRef(resolver.to);
        if (!parsed) {
            return {
                type: 'conflict',
                path: path.concat('to'),
                message: `Unable to parse property ref "${resolver.to}"`
            };
        }
        if (parsed.layerId !== 'local') {
            return;
        }
        const targetResolver = get(projectSchema, propertyRefItemToResolverPath(getNamespace, parsed));
        if (!isBasicResolver(targetResolver) && !isComposeResolver(targetResolver)) {
            return {
                type: 'notFound',
                path: basePath.concat('to'),
                message: `Missing resolver config at property ref "${resolver.to}"`
            };
        }
    };
    const validateAITGuardrail = (guardrail, basePath) => {
        if (guardrail.name === 'similarity') {
            if (!isValidShapeName(guardrail.shape)) {
                return {
                    type: 'notFound',
                    path: basePath.concat('shape'),
                    message: `Invalid shape "${guardrail.shape}" for ${guardrail.shape}, must use an indexed shape`
                };
            }
        }
    };
    const validateAIGenerateTextResolver = (resolver, path) => {
        if (resolver.tools) {
            for (let i = 0, n = resolver.tools.length; i < n; i++) {
                pushErrors(errors, validateAIToolConfig(projectSchema, getNamespace, resolver.tools[i], path.concat(['tools', i])));
            }
        }
        if (resolver.guardrails) {
            for (let i = 0, n = resolver.guardrails.length; i < n; i++) {
                pushErrors(errors, validateAITGuardrail(resolver.guardrails[i], path.concat(['guardrails', i])));
            }
        }
        const { toolChoice } = resolver.options ?? {};
        if (toolChoice === 'required') {
            if (!resolver.tools?.length) {
                errors.push({
                    type: 'notFound',
                    path: path.concat(['options', 'toolChoice']),
                    message: `Must have at least one tool configured to specify "required"`
                });
            }
        }
        else if (toolChoice && toolChoice !== 'auto' && toolChoice !== 'none') {
            const parsed = parsePropertyRef(toolChoice);
            if (parsed) {
                const matchingTool = resolver.tools?.find((tool) => isEqual(parsed, parsePropertyRef(getToolRef(tool))));
                if (!matchingTool) {
                    errors.push({
                        type: 'notFound',
                        path: path.concat(['options', 'toolChoice']),
                        message: `Missing tool "${toolChoice}" must be specified in "tools" array`
                    });
                }
            }
            else {
                errors.push({
                    type: 'conflict',
                    path: path.concat(['options', 'toolChoice']),
                    message: `Unable to parse property ref "${toolChoice}"`
                });
            }
        }
    };
    const validateChunkResolver = (resolver, path) => {
        if (resolver.options.type === 'semantic') {
            if (!projectSchema.services?.[resolver.options.serviceId]) {
                errors.push({
                    type: 'notFound',
                    path: path.concat(['options', 'serviceId']),
                    message: `Invalid service "${resolver.options.serviceId}"`
                });
            }
        }
    };
    const getNamespace = createGetNamespace(projectSchema);
    for (const [resolver, path] of enumerateBasicResolvers(baseResolver, basePath)) {
        if (isBasicResolver(resolver)) {
            if ('service' in resolver &&
                resolver.service !== 'shapedb' &&
                resolver.service !== 'takeshape' &&
                !projectSchema.services?.[resolver.service]) {
                errors.push({
                    type: 'notFound',
                    path: path.concat(['service']),
                    message: `Invalid service "${resolver.service}"`
                });
            }
            if (resolver.shapeName) {
                const { shapeName } = resolver;
                if (!isValidShapeName(shapeName)) {
                    errors.push({
                        type: 'notFound',
                        path: path.concat(['shapeName']),
                        message: `Invalid Model Shape "${shapeName ?? ''}"`
                    });
                }
            }
            if (resolver.name === 'delegate') {
                pushErrors(errors, validateDeletgateResolver(resolver, path));
            }
            else if (isAIResolver(resolver)) {
                validateAIGenerateTextResolver(resolver, path);
            }
            else if (resolver.name === 'util:chunk') {
                validateChunkResolver(resolver, path);
            }
        }
        else {
            errors.push({
                type: 'notFound',
                path,
                message: 'Invalid resolver'
            });
        }
    }
    return errors;
}
function validateLocalQueryConfig(context, projectSchema, query, operation, name) {
    const location = operation === 'query' ? 'queries' : 'mutations';
    return validateResolver(context, projectSchema, [location, name, 'resolver'], query.resolver);
}
const operationProps = [['query', 'queries'], ['mutation', 'mutations']];
function validateLocalQueryConfigs(context, projectSchema) {
    const errors = [];
    for (const [operation, prop] of operationProps) {
        for (const name of Object.keys(projectSchema[prop])) {
            errors.push(...validateLocalQueryConfig(context, projectSchema, projectSchema[prop][name], operation, name));
        }
    }
    return errors;
}
function allowDisconnected(context, status) {
    return Boolean(context.allowDisconnectedLayers) && status !== 'notAvailable';
}
function isValidShapeReference(context, layerState, shapeName) {
    if (layerState.status !== 'ok') {
        return allowDisconnected(context, layerState.status);
    }
    return Boolean(layerState.schema.shapes[shapeName]);
}
async function validateResolverReferences(context, basePath, baseResolver) {
    const errors = [];
    const isInvalidPropertyRef = async (refStr) => {
        const ref = parsePropertyRef(refStr);
        // ref string format and local refs are validated in validateResolver()
        if (!ref || ref.layerId === 'local') {
            return false;
        }
        const layerState = await context.resolveLayer(ref.layerId);
        return layerState.status === 'ok'
            ? !get(layerState.schema, propertyRefItemToPath(value(''), ref))
            : !allowDisconnected(context, layerState.status);
    };
    await pMap(enumerateBasicResolvers(baseResolver, basePath), async ([resolver, path]) => {
        if (resolver.name === 'graphql:query' || resolver.name === 'graphql:mutation') {
            const { service, fieldName } = resolver;
            const operationProp = resolver.name === 'graphql:query' ? 'query' : 'mutation';
            const { projectSchema: { services } } = context;
            const layerState = await context.resolveLayer(service);
            const { schema: layerSchema, status: layerStatus } = layerState;
            const valid = layerStatus === 'ok'
                ? Boolean(findSchemaAtQueryFieldPath({ ...layerSchema, services }, getQueryFieldPath(operationProp, fieldName)))
                : allowDisconnected(context, layerState.status);
            if (!valid) {
                errors.push({
                    type: 'notFound',
                    path: path.concat('fieldName'),
                    message: `Missing ${operationProp} "${resolver.fieldName}" in service layer "${service}"`
                });
            }
        }
        else if (resolver.name === 'delegate') {
            if (await isInvalidPropertyRef(resolver.to)) {
                errors.push({
                    type: 'notFound',
                    path: path.concat('to'),
                    message: `Missing resolver config at property ref "${resolver.to}"`
                });
            }
        }
        else if (isAIResolver(resolver)) {
            if (resolver.tools) {
                await pMap(resolver.tools, async (tool, i) => {
                    const toolRef = getToolRef(tool);
                    if (await isInvalidPropertyRef(toolRef)) {
                        errors.push({
                            type: 'notFound',
                            path: path.concat(typeof tool === 'string' ? ['tools', i] : ['tools', i, 'ref']),
                            message: `Invalid ref "${toolRef}"`
                        });
                    }
                });
            }
        }
    });
    return errors;
}
async function validateQueryConfig(context, { query, name, operation }) {
    const location = operation === 'query' ? 'queries' : 'mutations';
    return validateResolverReferences(context, [location, name, 'resolver'], query.resolver);
}
async function validateQueryConfigs(context, projectSchema) {
    const isLessThanV3_9_0 = lt(getSemver(projectSchema.schemaVersion), '3.9.0');
    if (isLessThanV3_9_0) {
        return [];
    }
    const promises = [];
    for (const [operation, prop] of operationProps) {
        for (const name of Object.keys(projectSchema[prop])) {
            promises.push(validateQueryConfig(context, {
                query: projectSchema[prop][name],
                operation,
                name
            }));
        }
    }
    return flatten(await Promise.all(promises));
}
function listShapeJoins(projectSchema) {
    const joins = [];
    for (const shapeKey of Object.keys(projectSchema.shapes)) {
        const shape = projectSchema.shapes[shapeKey];
        if (shape.joins) {
            for (const joinRef of Object.keys(shape.joins)) {
                const join = shape.joins[joinRef];
                joins.push({
                    ref: joinRef,
                    path: ['shapes', shapeKey, 'joins', joinRef],
                    join
                });
            }
        }
    }
    return joins;
}
function validateLocalShapeJoinRef(projectSchema, item) {
    const refItem = parseRef(projectSchema, item.ref);
    if (refItem.layerId === 'local') {
        const shape = refItemToShape(projectSchema, refItem);
        if (!shape) {
            return [
                {
                    type: 'notFound',
                    path: item.path,
                    message: `Invalid join "${item.ref}" not found`
                }
            ];
        }
    }
    return [];
}
function validateLocalShapeJoins(context, projectSchema) {
    return listShapeJoins(projectSchema).flatMap((item) => validateLocalShapeJoinRef(projectSchema, item).concat(validateResolver(context, projectSchema, item.path.concat('resolver'), item.join.resolver)));
}
async function validateShapeJoinRef(context, projectSchema, item) {
    const refItem = parseRef(projectSchema, item.ref);
    if (refItem) {
        const layer = await context.resolveLayer(refItem.layerId);
        const valid = isValidRef(context, projectSchema, layer, refItem);
        if (!valid) {
            return [
                {
                    type: 'notFound',
                    path: item.path,
                    message: `Invalid join "${item.ref}" not found`
                }
            ];
        }
    }
    return [];
}
async function validateShapeJoins(context, projectSchema) {
    const promises = [];
    for (const item of listShapeJoins(projectSchema)) {
        promises.push(validateShapeJoinRef(context, projectSchema, item), validateResolverReferences(context, item.path, item.join.resolver));
    }
    return flatten(await Promise.all(promises));
}
function validateShapeLoaders(context, projectSchema, shape) {
    const errors = [];
    if (shape.loaders) {
        const getNamespace = createGetNamespace(projectSchema);
        const listLoader = shape.loaders.list;
        if (listLoader) {
            ensureArray(listLoader).forEach((config, i) => {
                const ref = parsePropertyRef(config.query);
                const query = ref && get(projectSchema, propertyRefItemToPath(getNamespace, ref));
                if (!query) {
                    errors.push({
                        path: Array.isArray(listLoader)
                            ? ['shapes', shape.name, 'loaders', 'list', i, 'query']
                            : ['shapes', shape.name, 'loaders', 'list', 'query'],
                        type: 'notFound',
                        message: `Invalid query "${config.query}"`
                    });
                }
            });
        }
        const getLoader = shape.loaders.get;
        if (getLoader) {
            const ref = parsePropertyRef(getLoader.query);
            const query = ref && get(projectSchema, propertyRefItemToPath(getNamespace, ref));
            if (!query) {
                errors.push({
                    path: ['shapes', shape.name, 'loaders', 'get', 'query'],
                    type: 'notFound',
                    message: `Invalid query "${getLoader.query}"`
                });
            }
        }
    }
    if (shape.cache?.triggers && !context.ignoreEntitlements) {
        const min = context.entitlements.minScheduleTriggerInterval ?? DEFAULT_MIN_SCHEDULE_TRIGGER_INTERVAL;
        for (let i = 0; i < shape.cache.triggers.length; i++) {
            const trigger = shape.cache.triggers[i];
            if (trigger.type === 'schedule' && trigger.interval < min) {
                errors.push({
                    path: ['shapes', shape.name, 'cache', 'triggers', i, 'interval'],
                    type: 'entitlement',
                    message: `Trigger interval ${trigger.interval} is below minimum ${min}`
                });
            }
        }
    }
    return errors;
}
function visitProperties(projectSchema, callback) {
    JSONPath('$..properties.*', projectSchema, (property, _type, { path: propertyPath }) => {
        callback(property, propertyPath.slice(3, -2).split(`']['`));
    }, undefined);
}
function validateIndexedShapes(context, projectSchema) {
    const errors = [];
    const { shapes } = projectSchema;
    for (const shape of Object.values(shapes)) {
        const newErrors = validateShapeLoaders(context, projectSchema, shape);
        if (newErrors.length) {
            errors.push(...newErrors);
        }
    }
    let nestedCount = 0;
    visitProperties(projectSchema, (property, propertyPath) => {
        const indexed = property['@indexed'];
        if (isRecord(indexed) && indexed.nested) {
            nestedCount += 1;
            if (property.type !== 'array') {
                errors.push({
                    path: propertyPath,
                    type: 'location',
                    message: 'Nested indexes may only appear next to fields of type: "array"'
                });
            }
        }
    });
    if (nestedCount > 50) {
        errors.push({
            path: ['shapes'],
            type: 'tooMany',
            message: `${nestedCount} nested indexes found`
        });
    }
    return errors;
}
/**
 * Verify the path has an allOf schema and not just prop named "allOf"
 */
function isAllOfPath(path) {
    const index = path.indexOf('allOf');
    return index !== -1 && isIntegerLike(path[index + 1]);
}
function isLocalRef(refItem) {
    return refItem.layerId === 'local';
}
function validateLocalRefs(context, projectSchema) {
    const errors = [];
    const shapeNames = new Set([...context.builtInShapeNames, ...Object.keys(projectSchema.shapes)]);
    const refs = getAllRefs(projectSchema).filter(isLocalRef);
    for (const item of refs) {
        if (item.template && !isValidTemplate(item.template)) {
            errors.push({
                type: 'notFound',
                path: item.path,
                message: `Invalid template "${item.template}"`
            });
        }
        const shapeName = refItemToNamespacedShapeName(projectSchema, item);
        if (!shapeNames.has(shapeName)) {
            const invalidRef = get(projectSchema, item.path);
            const path = invalidRef.items ? [...item.path, 'items', '@ref'] : item.path;
            const ref = get(projectSchema, path);
            errors.push({
                type: 'notFound',
                path,
                message: `Invalid ref "${ref}"`
            });
        }
        // Make sure refs inside allOf don't refer to their own Shape
        if (item.path[1] === shapeName && isAllOfPath(item.path)) {
            errors.push({
                type: 'conflict',
                path: item.path,
                message: 'allOf cannot be self-referential'
            });
        }
        const parentPath = initial(item.path);
        const parentSchema = get(projectSchema, parentPath);
        const propName = last(item.path);
        if (propName === '@ref' && parentSchema.$ref) {
            errors.push({
                type: 'conflict',
                path: parentPath,
                message: 'Ref cannot have both @ref and $ref'
            });
        }
    }
    return errors;
}
function isValidRef(context, projectSchema, layer, item) {
    const { layerId } = item;
    const shapeName = refItemToNamespacedShapeName(projectSchema, item);
    const localShapeExists = Boolean(projectSchema.shapes[shapeName]) || context.builtInShapeNames.has(shapeName);
    return (isValidServiceId(projectSchema, layerId) &&
        (layerId === 'local' ? localShapeExists : localShapeExists || isValidShapeReference(context, layer, shapeName)));
}
function mustBeInterface({ path }) {
    return path[0] === 'shapes' && path[2] === 'interfaces';
}
async function validateRefs(context, projectSchema) {
    const { resolveLayer } = context;
    const errors = [];
    const refs = getAllRefs(projectSchema);
    const layerIds = new Set();
    for (const item of refs) {
        if (item.layerId !== 'local') {
            layerIds.add(item.layerId);
        }
    }
    const layersById = Object.fromEntries(await pMap(layerIds, async (layerId) => [layerId, await resolveLayer(layerId)]));
    for (const item of refs) {
        if (item.template && !isValidTemplate(item.template)) {
            errors.push({
                type: 'notFound',
                path: item.path,
                message: `Invalid template "${item.path}"`
            });
        }
        const { layerId } = item;
        const layer = layersById[layerId];
        const valid = isValidRef(context, projectSchema, layer, item);
        if (!valid) {
            errors.push({
                type: 'notFound',
                path: item.path,
                message: `Invalid ref "${get(projectSchema, item.path)}"`
            });
        }
        else if (mustBeInterface(item) && (layerId === 'local' || layer?.status === 'ok')) {
            // only validate interfaces with connected layers isValidRef above handles when allowDisconnectedLayers false
            const context = layer?.schema ?? projectSchema;
            const shape = get(context, item.path.slice(0, 2));
            const layerWithServices = { ...context, services: projectSchema.services };
            errors.push(...validateShapeImplementsInterface(layerWithServices, shape, item, item.path));
        }
    }
    return errors;
}
function visitShapePropertiesJson(projectSchema, callback) {
    JSONPath('shapes.*.schema.properties.*', projectSchema, (property, _type, { path: propertyPath }) => {
        callback(property, propertyPath.slice(3, -2).split(`']['`));
    }, undefined);
}
function validateDirectives(context, projectSchema) {
    const errors = [];
    visitShapePropertiesJson(projectSchema, (property, propertyPath) => {
        const resolver = property['@resolver'];
        if (resolver) {
            const propPath = propertyPath.concat('@resolver');
            const resolverErrors = validateResolver(context, projectSchema, propPath, resolver);
            if (resolverErrors) {
                errors.push(...resolverErrors);
            }
        }
    });
    return errors;
}
function validateOneOfs(projectSchema) {
    const errors = [];
    visitShapePropertiesJson(projectSchema, (property, propertyPath) => {
        const oneOfPath = property.items ? ['items', 'oneOf'] : ['oneOf'];
        const relationship = getRelationship(property);
        const schema = (property.items ? property.items : property);
        const isUnion = isUnionSchema(schema);
        if (schema.oneOf && !relationship && !isUnion && !isEnumLikeSchema(schema)) {
            errors.push({
                type: 'conflict',
                path: propertyPath.concat(oneOfPath),
                message: 'Invalid oneOf must contain only @ref or title + enum/const schemas'
            });
        }
        if (isUnion &&
            schema.oneOf.length !== uniqBy(schema.oneOf, (refSchema) => getRefShapeName(projectSchema, refSchema)).length) {
            errors.push({
                type: 'conflict',
                path: propertyPath.concat(oneOfPath),
                message: 'Invalid oneOf must not contain duplicate references'
            });
        }
    });
    return errors;
}
function validateLocales(projectSchema) {
    const { locales, defaultLocale } = projectSchema;
    const errors = [];
    if (locales && !locales.includes(defaultLocale)) {
        errors.push({
            type: 'notFound',
            path: ['locales'],
            message: `Default locale "${defaultLocale}" missing from locales`
        });
    }
    return errors;
}
const importantFields = ['type', '@ref', 'allOf', 'oneOf', 'anyOf', 'extends', 'items', '@tag'];
function isSchemaEqual(a, b) {
    return isEqual(pick(a, importantFields), pick(b, importantFields));
}
function matchesInterface(shape, interfaceShape, path) {
    const errors = [];
    const { schema } = shape;
    const interfaceSchema = interfaceShape.schema;
    for (const name of Object.keys(interfaceSchema.properties)) {
        const propSchema = schema.properties[name];
        const interfacePropSchema = interfaceSchema.properties[name];
        if (!propSchema) {
            errors.push({
                type: 'notFound',
                path,
                message: `"${shape.name}" is missing property "${name}" required by interface "${interfaceShape.name}"`
            });
        }
        else if (!isSchemaEqual(propSchema, interfacePropSchema)) {
            errors.push({
                type: 'conflict',
                path: [...path, name],
                message: `"${shape.name}.${name}" must match "${interfaceShape.name}.${name}"`
            });
        }
        else {
            const propRequired = Boolean(schema.required?.includes(name));
            const interfacePropRequired = Boolean(interfaceSchema.required?.includes(name));
            if (interfacePropRequired !== propRequired) {
                errors.push({
                    type: 'conflict',
                    path: [...path, name],
                    message: `"${shape.name}.${name}" must ${interfacePropRequired ? '' : 'not '}be required to  match "${interfaceShape.name}.${name}"`
                });
            }
        }
    }
    return errors;
}
function validateShapeImplementsInterface(projectSchema, shape, interfaceRefItem, path) {
    const errors = [];
    const interfaceShape = refItemToShape(projectSchema, interfaceRefItem);
    if (!interfaceShape) {
        errors.push({
            type: 'notFound',
            path,
            message: `Invalid interface "${serializeRef(interfaceRefItem)}" not found`
        });
    }
    else if (interfaceShape.type !== 'interface') {
        errors.push({
            type: 'conflict',
            path,
            message: `"${shape.name}" cannot implement non-interface "${interfaceShape.name}"`
        });
    }
    else if (isExtendsSchema(shape.schema)) {
        // TODO stricter extends validation needs https://app.shortcut.com/takeshape/story/9783/use-service-schemas-during-schema-validation
    }
    else if (isObjectSchema(shape.schema) && isObjectSchema(interfaceShape.schema)) {
        const matchErrors = matchesInterface(shape, interfaceShape, [
            'shapes',
            shape.name,
            'schema',
            'properties'
        ]);
        if (matchErrors.length) {
            errors.push(...matchErrors);
        }
    }
    return errors;
}
function validateInterfaceImplementations(projectSchema) {
    const errors = [];
    for (const shape of Object.values(projectSchema.shapes)) {
        if (shape.interfaces?.length) {
            if (!isObjectSchema(shape.schema) && !isExtendsSchema(shape.schema)) {
                return [
                    {
                        path: ['shapes', shape.name, 'schema'],
                        type: 'conflict',
                        message: `"${shape.name}" must have an object or extends schema to implement interfaces`
                    }
                ];
            }
        }
    }
    return errors;
}
function validateInterfaces(projectSchema) {
    const errors = [];
    // Validate interface shapes must be an object schema with at least one property
    for (const shape of Object.values(projectSchema.shapes)) {
        if (shape.type === 'interface') {
            if (shape.model) {
                errors.push({
                    path: ['shapes', shape.name, 'model'],
                    type: 'conflict',
                    message: `"${shape.name}" may not be both an interface and model. Create a separate model shape which implements this interface.`
                });
            }
            else if (isObjectSchema(shape.schema)) {
                if (size(shape.schema.properties) === 0) {
                    errors.push({
                        path: ['shapes', shape.name, 'schema', 'properties'],
                        type: 'conflict',
                        message: `Interface "${shape.name}" must define at least one property`
                    });
                }
            }
            else {
                errors.push({
                    path: ['shapes', shape.name, 'schema'],
                    type: 'conflict',
                    message: `Interface "${shape.name}" must define an object schema`
                });
            }
        }
    }
    return errors;
}
function isValidateReferencesContext(context) {
    return Boolean(context.resolveLayer);
}
const ajv = createAjv();
function validateStructure(schemaVersion, context, schema, ref) {
    const coerced = coerce(schemaVersion);
    const versionStr = coerced?.format();
    const hasExperimental = Boolean(coerced && gte(coerced, '3.48.0') && lt(coerced, '3.57.0'));
    const relevantSchemas = allProjectSchemas.filter((metaSchema) => (hasExperimental && metaSchema.$id.endsWith('experimental-2025-07#')) ||
        metaSchema.$id.endsWith(`v${versionStr}#`));
    for (const relevantSchema of relevantSchemas) {
        if (!ajv.getSchema(relevantSchema.$id)) {
            ajv.addSchema(relevantSchema);
        }
    }
    ajv.validate(`https://schema.takeshape.io/project-schema/v${versionStr}#${ref ?? ''}`, schema);
    let errors = ajv.errors?.map(formatError) ?? [];
    const { suppressErrors } = context;
    if (errors.length && suppressErrors) {
        errors = errors.filter((error) => !suppressErrors(error));
    }
    if (errors.length) {
        return {
            valid: false,
            schema: undefined,
            errors
        };
    }
    return {
        valid: true,
        schema: schema,
        errors: undefined
    };
}
function validateGuards(context, schema) {
    const errors = [];
    if (context.ignoreEntitlements) {
        return errors;
    }
    const numGuards = getGuards(schema).filter(isGuardEnabled).length;
    const entitledGuards = context.entitlements.guards ?? 0;
    if (numGuards > entitledGuards) {
        errors.push({
            type: 'entitlement',
            message: `Number of enabled guards exceeds your entitled limit of ${entitledGuards}`,
            path: [...GUARDS_SCHEMA_PATH]
        });
    }
    return errors;
}
function formatValidationResult(context, errors, schema) {
    const { suppressErrors } = context;
    if (suppressErrors) {
        errors = errors.filter((error) => !suppressErrors(error));
    }
    return errors.length ? { valid: false, schema: undefined, errors } : { valid: true, schema, errors: undefined };
}
function validateSyntax(context, schema) {
    let errors = [];
    errors = errors
        .concat(checkShapeNames(schema.shapes))
        .concat(checkShapeIds(schema.shapes))
        .concat(validateWorkflows(schema))
        .concat(validateLocalQueryConfigs(context, schema))
        .concat(validateLocalRefs(context, schema))
        .concat(validateLocalShapeJoins(context, schema))
        .concat(validateDirectives(context, schema))
        .concat(validateLocales(schema))
        .concat(checkWorkflowStepNames(schema.workflows))
        .concat(checkWorkflowStepKeys(schema.workflows))
        .concat(validateOneOfs(schema))
        .concat(validateIndexedShapes(context, schema))
        .concat(validateInterfaces(schema))
        .concat(validateInterfaceImplementations(schema))
        .concat(validateAgents(schema))
        .concat(validateGuards(context, schema));
    const mcpTools = getMcpToolMap(schema);
    if (mcpTools) {
        errors = errors.concat(checkToolNames(mcpTools ?? {}));
    }
    return formatValidationResult(context, errors, schema);
}
async function validateReferences(context, schema) {
    const errors = flatten(await Promise.all([
        validateRefs(context, schema),
        validateQueryConfigs(context, schema),
        validateShapeJoins(context, schema)
    ]));
    return formatValidationResult(context, errors, schema);
}
const schemaUndefinedResult = {
    valid: false,
    schema: undefined,
    errors: [{ path: [], type: 'undefined', message: 'Schema is undefined' }]
};
const invalidVersionResult = {
    valid: false,
    schema: undefined,
    errors: [{ path: [], type: 'invalidVersion', message: 'Unknown schema version' }]
};
function normalizeSchemaVersion(obj) {
    if (isRecord(obj)) {
        return typeof obj.schemaVersion === 'string' ? obj.schemaVersion : '1';
    }
}
export function validateSchemaSyntax(obj, options = {}) {
    if (isUndefined(obj)) {
        return schemaUndefinedResult;
    }
    const schemaVersion = normalizeSchemaVersion(obj);
    if (!schemaVersion) {
        return invalidVersionResult;
    }
    return validateStructure(schemaVersion, options, obj);
}
/**
 * Validates a schema using a matching validation based on the `schemaVersion` property.
 */
export async function validateSchema(options, obj) {
    if (isUndefined(obj)) {
        return schemaUndefinedResult;
    }
    const builtInShapes = getBuiltInShapes({ entitlements: options.entitlements ?? DEFAULT_ENTITLEMENTS });
    const builtInShapeNames = new Set([...Object.keys(builtInShapes), ...scalars, 'object']);
    const context = {
        allowDisconnectedLayers: true,
        builtInShapes,
        builtInShapeNames,
        ...options
    };
    const schemaVersion = normalizeSchemaVersion(obj);
    if (!schemaVersion) {
        return invalidVersionResult;
    }
    const structuralValidation = validateStructure(schemaVersion, context, obj);
    if (!structuralValidation.valid || context.structureOnly) {
        return structuralValidation;
    }
    const { schema } = structuralValidation;
    if (!isLatestProjectSchemaJSON(schema)) {
        return invalidVersionResult;
    }
    const syntaxValidation = validateSyntax(context, schema);
    const validateReferencesContext = {
        ...context,
        projectSchema: schema
    };
    if (!syntaxValidation.valid || !isValidateReferencesContext(validateReferencesContext)) {
        return syntaxValidation;
    }
    return validateReferences(validateReferencesContext, schema);
}
export function ensureValidLatestSchemaSyntax(obj) {
    const { valid, schema, errors } = validateSchemaSyntax(obj);
    if (!valid && errors) {
        throw new Error(`Invalid Schema "${errors[0].path.join(',')}": "${errors[0].message}"`);
    }
    if (isUndefined(schema)) {
        throw new Error('Invalid schema: schema is undefined');
    }
    if (neq(CURRENT_SCHEMA_VERSION, coerce(schema.schemaVersion) ?? LEGACY_SCHEMA_VERSION)) {
        throw new Error(`Expected schemaVersion: "${CURRENT_SCHEMA_VERSION}", got "${schema.schemaVersion}"`);
    }
    return schema;
}
/**
 * The roles import validator ensures required properties, and strips several
 * which might exist in an export but wouldn't apply to an import operation.
 */
function createRoleInputValidator() {
    const validator = createAjv({ useDefaults: true, removeAdditional: true });
    const { RoleInput, ...definitions } = authSchemas.definitions;
    return validator.compile({
        $id: 'RoleInput',
        definitions,
        ...RoleInput
    });
}
/**
 * Validates and returns a `RoleInput`, which is a subset of the `Role`
 * type's properties suitable for importing.
 */
export function validateRoleInput(maybeRoleInput) {
    const validate = createRoleInputValidator();
    if (validate(maybeRoleInput)) {
        return maybeRoleInput;
    }
    throw new SchemaValidationError('RoleInput was invalid', validate.errors?.map(formatError));
}
/**
 * The roles import validator ensures required properties, and strips several
 * which might exist in an export but wouldn't apply to an import operation.
 */
function createRoleImportValidator() {
    const validator = createAjv({ useDefaults: true, removeAdditional: true });
    const { RoleImport, ...definitions } = authSchemas.definitions;
    return validator.compile({
        $id: 'RoleImport',
        definitions,
        ...RoleImport
    });
}
/**
 * Validates and returns a `RoleImport` array, which is a collection of `RoleInput`s.
 */
export function ensureValidRoleImport(maybeRoles) {
    const validate = createRoleImportValidator();
    if (validate(maybeRoles)) {
        return maybeRoles;
    }
    throw new SchemaValidationError('Roles were invalid', validate.errors?.map(formatError));
}
export function suppressPaths(paths) {
    return (error) => {
        return paths.includes(error.path.join('.'));
    };
}
/**
 * Only use when validating an imported schema! ignore fields optional when importing
 */
export function validateProjectSchemaImport(maybeSchema) {
    return validateSchemaSyntax(maybeSchema, {
        suppressErrors: suppressPaths([...projectSchemaImportOptionalProps, ...legacyProjectSchemaImportOptionalProps])
    });
}
/**
 * Only use when validating an imported schema!
 *
 * New style validation for parity with the `validateRoleImport` fn. Returns
 * a valid, clean `ImportProjectSchema` or throws a `SchemaValidationError`.
 */
export function ensureValidProjectSchemaImport(maybeSchema) {
    const validateResult = validateProjectSchemaImport(maybeSchema);
    if (validateResult.valid) {
        return validateResult.schema;
    }
    throw new SchemaValidationError('ProjectSchema could not be validated', validateResult.errors);
}
