import { DEFAULT_ENTITLEMENTS, deepClone, ensureArray, pascalCase, visit } from '@takeshape/util';
import curry from 'lodash/fp/curry.js';
import orderBy from 'lodash/fp/orderBy.js';
import isArray from 'lodash/isArray.js';
import isString from 'lodash/isString.js';
import keyBy from 'lodash/keyBy.js';
import mapValues from 'lodash/mapValues.js';
import uniq from 'lodash/uniq.js';
import { AGENTS_SCHEMA_PATH, getAgentMap } from './agents.js';
import { workflowsEnabled } from "./api-version.js";
import { allBuiltInShapes, getBuiltInForms, getBuiltInShapes } from "./builtin-schema.js";
import { SERVICE_OBJECT_PATTERN_NAME } from "./constants.js";
import { createTemplateShapeName, dereferenceObjectSchema, dereferenceSchema, getRef, getRefOrItemsRef, getRefShapeName, isValidRefItem, parseName, parseRef, parseReturnShape, refItemToNamespacedShapeName, refItemToShape, refItemToShapeSchema, refToRefItem, returnShapeToSchema, serializePropertyRef } from "./refs.js";
import { getArgsType, isAllOfSchema, isArraySchema, isCachedShape, isExtendsSchema, isGraphqlResolver, isModelShape, isObjectSchema, isOneOfSchema, isReferenceableShape, isRefSchema } from "./types/index.js";
import { isUnionSchema } from "./unions.js";
import { getArgs } from "./util/has-arg.js";
import { mergeFormProperties, mergeSchemaProperties } from "./util/merge.js";
import { CURRENT_API_VERSION, CURRENT_SCHEMA_VERSION } from "./versions.js";
import { defaultWorkflow, getStatusField } from "./workflows.js";
export function isBuiltinShape(ref) {
    const parsed = parseRef({}, ref);
    return Boolean(parsed.layerId === 'local' && allBuiltInShapes[parsed.shapeName]);
}
const builtInQueriesAndMutations = getShapeQueriesAndMutations(Object.values(allBuiltInShapes));
export function isBuiltinQuery(name) {
    return builtInQueriesAndMutations.queries[name] !== undefined;
}
export function isBuiltinMutation(name) {
    return builtInQueriesAndMutations.mutations[name] !== undefined;
}
export function isBuiltinType(projectSchema, refSchema) {
    const shapeName = getRefShapeName(projectSchema, refSchema);
    return shapeName !== undefined && isBuiltinShape(shapeName);
}
export const schemaMetadataKeys = [
    '$comment',
    'title',
    'description',
    '@l10n',
    '@deprecationReason',
    '@tag',
    '@tags',
    '@syncLocaleStructure',
    '@sensitive'
];
export const commonSchemaKeys = ['type', 'default', 'readOnly', ...schemaMetadataKeys];
export const scalarSchemaKeys = [
    ...commonSchemaKeys,
    'format',
    'oneOf',
    'multipleOf',
    'maximum',
    'exclusiveMaximum',
    'minimum',
    'exclusiveMinimum',
    'maxLength',
    'minLength',
    'pattern'
];
export const objectSchemaKeys = [...commonSchemaKeys, 'properties', 'required'];
export const arraySchemaKeys = [
    ...commonSchemaKeys,
    'items',
    'additionalItems',
    'maxItems',
    'minItems',
    'uniqueItems'
];
export const multipleRelationshipSchemaKeys = [...schemaMetadataKeys, 'maxItems', 'minItems'];
// All properties except properties, items, additionalItems
export const nonStructuralSchemaKeys = [
    ...scalarSchemaKeys,
    '@resolver',
    '@input',
    '@output',
    '@backreference',
    '@args',
    'required',
    'maxItems',
    'minItems',
    'uniqueItems'
];
export function getServiceIdFieldName(serviceFieldName) {
    return `${serviceFieldName}Id`;
}
const generatorVersions = {
    [SERVICE_OBJECT_PATTERN_NAME]: '1'
};
export function getGeneratorVersion(generator) {
    return generatorVersions[generator];
}
// Follow refs to find the path between two shapes in a schema
export function pathBetween(fromShape, toShape, projectSchema, depthLimit = 4) {
    if (fromShape.name === toShape.name) {
        return [];
    }
    const toShapeName = toShape.name;
    let shortestPath;
    let shortestPathLength = Number.MAX_SAFE_INTEGER;
    const pathBetweenHelper = (schema, pathSoFar, seen, depth) => {
        if (depth > depthLimit) {
            return;
        }
        if (schema.items) {
            pathBetweenHelper(schema.items, pathSoFar, seen, depth);
            return;
        }
        const refItem = getRef(projectSchema, schema);
        if (refItem) {
            const referenced = refItemToShape(projectSchema, refItem);
            if (referenced) {
                if (referenced.name === toShapeName && pathSoFar.length < shortestPathLength) {
                    shortestPath = pathSoFar;
                    shortestPathLength = pathSoFar.length;
                }
                else if (!seen.has(referenced.name)) {
                    pathBetweenHelper(referenced.schema, pathSoFar, new Set([...seen, referenced.name]), depth);
                }
            }
        }
        else if (pathSoFar.length + 1 < shortestPathLength && (schema.oneOf ?? schema.allOf ?? schema.properties)) {
            if (schema.oneOf) {
                for (const itemSchema of schema.oneOf) {
                    pathBetweenHelper(itemSchema, pathSoFar, seen, depth);
                }
            }
            else {
                for (const [propName, propSchema] of createSchemaPropertyList(projectSchema, schema).getNodes()) {
                    pathBetweenHelper(propSchema, pathSoFar.concat(String(propName)), seen, depth + 1);
                }
            }
        }
    };
    pathBetweenHelper(fromShape.schema, [], new Set([fromShape.name]), 0);
    if (!shortestPath) {
        throw new Error(`No path between ${fromShape.name} and ${toShape.name}`);
    }
    return shortestPath;
}
export function getShapeQueriesAndMutations(shapes) {
    const queries = {};
    const mutations = {};
    for (const shape of ensureArray(shapes)) {
        const { name, model } = shape;
        const service = 'shapedb';
        if (model) {
            const isSingleton = model.type === 'single';
            queries[`get${name}`] = {
                shape: name,
                resolver: {
                    name: 'shapedb:get',
                    service,
                    shapeName: name
                },
                description: `Get a ${name} by ID`,
                args: isSingleton
                    ? createTemplateShapeName('TSGetSingletonArgs', name)
                    : createTemplateShapeName('TSGetArgs', name)
            };
            mutations[`update${name}`] = {
                shape: createTemplateShapeName('UpdateResult', name),
                resolver: {
                    name: 'shapedb:update',
                    service,
                    shapeName: name
                },
                description: `Update ${name}`,
                args: createTemplateShapeName('UpdateArgs', name)
            };
            if (!isSingleton) {
                queries[`get${name}List`] = {
                    shape: createTemplateShapeName('PaginatedList', name),
                    resolver: {
                        name: 'shapedb:list',
                        service,
                        shapeName: name
                    },
                    description: `Returns a list ${name} in natural order.`,
                    args: createTemplateShapeName('TSListArgs', name)
                };
                mutations[`create${name}`] = {
                    shape: createTemplateShapeName('CreateResult', name),
                    resolver: {
                        name: 'shapedb:create',
                        service,
                        shapeName: name
                    },
                    description: `Create ${name}`,
                    args: createTemplateShapeName('CreateArgs', name)
                };
                mutations[`duplicate${name}`] = {
                    shape: createTemplateShapeName('DuplicateResult', name),
                    resolver: {
                        name: 'shapedb:duplicate',
                        service,
                        shapeName: name
                    },
                    description: `Duplicate ${name}`,
                    args: createTemplateShapeName('DuplicateArgs', name)
                };
                mutations[`delete${name}`] = {
                    shape: createTemplateShapeName('DeleteResult', name),
                    resolver: {
                        name: 'shapedb:delete',
                        service,
                        shapeName: name
                    },
                    description: `Delete ${name}`,
                    args: createTemplateShapeName('DeleteArgs', name)
                };
            }
        }
    }
    return { queries, mutations };
}
export function getCommonBuiltInProperties(shape) {
    return {
        _id: {
            title: 'Id',
            type: 'string'
        },
        _shapeId: {
            type: 'string',
            title: 'Shape Id',
            enum: [shape.id]
        },
        _score: {
            type: 'number',
            title: 'Score',
            description: 'The relevance score for this result. Only populated in search contexts where scoring is applied (e.g., using "should" with boost).'
        }
    };
}
export const DEFAULT_ID_FIELD = 'id';
export function getIdField(obj) {
    return obj?.idField ?? DEFAULT_ID_FIELD;
}
/**
 * Get properties necessary for cached / remote data to work with the TSSearchable interface.
 */
function getCachedBuiltInProperties(projectSchema, shape, properties) {
    const idField = getIdField(shape.cache);
    const idFieldSchema = properties[idField];
    let idMapping;
    // Determine _id mapping: prefer existing mapping on idField, fallback to constructing from shape name
    if (idFieldSchema) {
        idMapping = idFieldSchema['@mapping'];
        if (!idMapping) {
            const { layerId, name } = parseName(projectSchema, shape.name);
            idMapping = `${layerId}:${name}.${idField}`;
        }
    }
    // TODO Implement `searchSummary` and `_shapeId` here, and remove `getRemoteShapeTSSearchableFields`
    return {
        _id: {
            title: 'Id',
            type: 'string',
            ...properties._id,
            ...(idMapping ? { '@mapping': idMapping } : {})
        }
    };
}
export function getModelBuiltInProperties(projectSchema, shape) {
    const apiVersion = projectSchema.apiVersion ?? CURRENT_API_VERSION;
    const showDeprecationReason = apiVersion !== '1';
    const properties = {
        _version: {
            title: 'Version',
            type: 'integer'
        },
        _shapeName: {
            title: 'Shape Name',
            type: 'string'
        },
        _createdAt: {
            title: 'Created',
            type: 'string',
            format: 'date-time'
        },
        _createdBy: {
            title: 'Created By',
            '@ref': 'local:TSUser',
            '@input': {
                type: 'string'
            },
            '@resolver': {
                name: 'takeshape:getUser',
                service: 'takeshape'
            }
        },
        _updatedAt: {
            title: 'Updated',
            type: 'string',
            format: 'date-time'
        },
        _updatedBy: {
            title: 'Updated By',
            '@ref': 'local:TSUser',
            '@input': {
                type: 'string'
            },
            '@resolver': {
                name: 'takeshape:getUser',
                service: 'takeshape'
            }
        },
        _schemaVersion: {
            title: 'Schema Version',
            type: 'number'
        },
        _enabled: {
            title: 'Enabled',
            type: 'boolean',
            ...(showDeprecationReason ? { '@deprecationReason': 'Use _status instead' } : {})
        },
        _enabledAt: {
            title: 'Enabled Date',
            type: 'string',
            format: 'date-time',
            ...(showDeprecationReason ? { '@deprecationReason': 'Use a custom date field instead' } : {})
        }
    };
    if (workflowsEnabled(apiVersion)) {
        return {
            ...properties,
            _status: {
                ...(projectSchema.workflows && getStatusField(projectSchema.workflows, shape))
            }
        };
    }
    return properties;
}
/**
 * Return all the built in property names,
 */
export function getBuiltInPropertyNames(profile = BuiltInPropertiesProfile.Model) {
    const fakeShape = {
        id: 'shape-id'
    };
    let keys = [...Object.keys(getCommonBuiltInProperties(fakeShape))];
    if (profile === BuiltInPropertiesProfile.Model) {
        keys = [...keys, ...Object.keys(getModelBuiltInProperties({}, fakeShape))];
    }
    return new Set(keys);
}
export var BuiltInPropertiesProfile;
(function (BuiltInPropertiesProfile) {
    BuiltInPropertiesProfile[BuiltInPropertiesProfile["Model"] = 0] = "Model";
    BuiltInPropertiesProfile[BuiltInPropertiesProfile["Cached"] = 1] = "Cached";
    BuiltInPropertiesProfile[BuiltInPropertiesProfile["None"] = 2] = "None";
})(BuiltInPropertiesProfile || (BuiltInPropertiesProfile = {}));
const BUILT_IN_PROPERTY_NAMES = {
    [BuiltInPropertiesProfile.Model]: getBuiltInPropertyNames(BuiltInPropertiesProfile.Model),
    [BuiltInPropertiesProfile.Cached]: getBuiltInPropertyNames(BuiltInPropertiesProfile.Cached),
    [BuiltInPropertiesProfile.None]: getBuiltInPropertyNames(BuiltInPropertiesProfile.None)
};
export const isBuiltInPropertyName = (name, profile = BuiltInPropertiesProfile.Model) => {
    return BUILT_IN_PROPERTY_NAMES[profile].has(name);
};
/**
 * Get the profile to use for loading built-in properties
 */
function getBuiltInPropertiesProfile(shape) {
    if (isModelShape(shape)) {
        return BuiltInPropertiesProfile.Model;
    }
    if (isCachedShape(shape) || isReferenceableShape(shape)) {
        return BuiltInPropertiesProfile.Cached;
    }
    return BuiltInPropertiesProfile.None;
}
export function applyBuiltinPropertiesToShape(projectSchema, shape) {
    const profile = getBuiltInPropertiesProfile(shape);
    if (profile === BuiltInPropertiesProfile.None) {
        return shape;
    }
    const shapeObjectSchema = dereferenceObjectSchema(projectSchema, shape.schema, ['shapes', shape.name]);
    const { properties } = shapeObjectSchema;
    const commonProperties = getCommonBuiltInProperties(shape);
    const modelProperties = profile === BuiltInPropertiesProfile.Model ? getModelBuiltInProperties(projectSchema, shape) : undefined;
    const cachedProperties = profile === BuiltInPropertiesProfile.Cached
        ? getCachedBuiltInProperties(projectSchema, shape, properties)
        : undefined;
    return {
        ...shape,
        schema: {
            ...shapeObjectSchema,
            properties: {
                ...properties,
                ...commonProperties,
                ...modelProperties,
                ...cachedProperties
            }
        }
    };
}
// Dependencies of these built-in shapes are automatically added by getBuiltinsUsed
const requiredBuiltinShapes = ['Asset', 'TSRelationship', 'TSStaticSite', 'TSUser'];
/**
 * Get all built-in shapes that are depended on by the given project schema.
 * Required built-in shapes are always included.
 */
export function getBuiltinsUsed(builtInShapes, projectSchema) {
    const builtinsUsed = {};
    const shapeNames = uniq(requiredBuiltinShapes.concat(getAllRefs(projectSchema, (refItem) => refItem.layerId === 'local' && isBuiltinShape(refItem.shapeName)).map((ref) => refItemToNamespacedShapeName(projectSchema, ref))));
    const schemaWithAllBuiltins = {
        ...emptySchema('project-id'),
        shapes: builtInShapes
    };
    for (const builtinShapeName of shapeNames) {
        if (builtInShapes[builtinShapeName]) {
            builtinsUsed[builtinShapeName] = builtInShapes[builtinShapeName];
            for (const depShapeName of getShapeDependencies(schemaWithAllBuiltins, builtInShapes[builtinShapeName])) {
                builtinsUsed[depShapeName] = builtInShapes[depShapeName];
            }
        }
    }
    return builtinsUsed;
}
export function applyDefaultsToSchema(projectSchema, options) {
    const optionsWithDefaults = {
        entitlements: DEFAULT_ENTITLEMENTS,
        ...options
    };
    const builtInShapes = getBuiltInShapes(optionsWithDefaults);
    const builtinsUsed = getBuiltinsUsed(builtInShapes, projectSchema);
    const builtInQueriesAndMutations = getShapeQueriesAndMutations(Object.values(builtInShapes));
    const builtInForms = getBuiltInForms(optionsWithDefaults);
    return {
        ...projectSchema,
        workflows: {
            ...projectSchema.workflows,
            ...(workflowsEnabled(projectSchema.apiVersion) && {
                default: defaultWorkflow
            })
        },
        queries: {
            ...builtInQueriesAndMutations.queries,
            ...projectSchema.queries
        },
        mutations: {
            ...builtInQueriesAndMutations.mutations,
            ...projectSchema.mutations
        },
        shapes: mapValues({ ...builtinsUsed, ...projectSchema.shapes }, (shape) => {
            const builtInShape = builtinsUsed[shape.name];
            if (builtInShape && builtInShape !== shape) {
                shape = {
                    ...shape,
                    schema: mergeSchemaProperties(dereferenceObjectSchema(projectSchema, shape.schema), dereferenceObjectSchema(projectSchema, builtInShape.schema))
                };
            }
            return applyBuiltinPropertiesToShape(projectSchema, shape);
        }),
        forms: mapValues({ ...builtInForms, ...projectSchema.forms }, (formsConfig, shapeName) => {
            const builtInFormsConfig = builtInForms[shapeName];
            if (builtInFormsConfig && builtInFormsConfig !== formsConfig) {
                formsConfig = {
                    ...formsConfig,
                    default: mergeFormProperties(formsConfig.default, builtInFormsConfig.default)
                };
            }
            return formsConfig;
        })
    };
}
export function getArgsReference(projectSchema, argsSchema) {
    const args = getArgs(argsSchema);
    if (args && typeof args === 'string') {
        // @args: ShapeName OR Namespace_TypeName
        return parseRef(projectSchema, args);
    }
}
export function getArgsReferenceWithPath(context, argsSchema, schemaPath = []) {
    const refItem = getArgsReference(context, argsSchema);
    if (refItem) {
        const argsType = getArgsType(argsSchema);
        return {
            ...refItem,
            path: schemaPath.concat([argsType])
        };
    }
}
export function getArgsShapeSchema(context, argsSchema) {
    const args = getArgs(argsSchema);
    if (args) {
        if (typeof args === 'string') {
            const refItem = getArgsReference(context, argsSchema);
            if (refItem) {
                return refItemToShapeSchema(context, refItem);
            }
        }
        else {
            return args;
        }
    }
}
const createShapeToPropertySchema = (projectSchema, definitions, definitionNames, { useShapeIdDiscriminator, additionalProperties }) => {
    const rewriteRefs = (shape) => {
        const handleSchema = (propSchema) => {
            // Unwrap array schema
            propSchema = propSchema.items ?? propSchema;
            if (isOneOfSchema(propSchema)) {
                const isUnion = isUnionSchema(propSchema);
                if (useShapeIdDiscriminator && isUnion) {
                    propSchema.discriminator = {
                        propertyName: '_shapeId'
                    };
                    propSchema.required = ['_shapeId'];
                }
                for (const schema of propSchema.oneOf) {
                    handleSchema(schema);
                }
                return;
            }
            // Add workflow validation
            if (propSchema['@workflow']) {
                const workflowName = propSchema['@workflow'];
                const workflow = projectSchema.workflows[workflowName] || defaultWorkflow;
                propSchema.oneOf = workflow.steps.map((step) => ({
                    enum: [step.key, step.name],
                    title: step.title
                }));
            }
            const refItem = getRef(projectSchema, propSchema);
            if (refItem) {
                const shapeName = refItemToNamespacedShapeName(projectSchema, refItem);
                const shape = refItemToShape(projectSchema, refItem);
                if (shape) {
                    delete propSchema['@ref'];
                    propSchema.$ref = `#/definitions/${shapeName}`;
                    if (!definitionNames.has(shapeName)) {
                        definitionNames.add(shapeName);
                        const rewritten = rewriteRefs(shape);
                        if (isObjectSchema(rewritten)) {
                            if (useShapeIdDiscriminator) {
                                rewritten.properties._shapeId = {
                                    type: 'string',
                                    enum: [shape.id]
                                };
                            }
                            if (additionalProperties !== undefined) {
                                rewritten.additionalProperties = additionalProperties;
                            }
                        }
                        definitions[shapeName] = rewritten;
                    }
                }
            }
        };
        const shapeSchema = deepClone(shape.schema);
        if (isOneOfSchema(shapeSchema)) {
            handleSchema(shapeSchema);
            return shapeSchema;
        }
        if (shapeSchema.enum) {
            return shapeSchema;
        }
        // An empty object schema is valid, allowing anything, but will fail dereferencing
        if (!Object.keys(shapeSchema).length) {
            return shapeSchema;
        }
        const shapeSchemaCopy = dereferenceObjectSchema(projectSchema, shapeSchema);
        if (shape.title) {
            shapeSchemaCopy.title = shape.title;
        }
        for (const schema of Object.values(shapeSchemaCopy.properties)) {
            handleSchema(schema);
        }
        if (additionalProperties !== undefined) {
            shapeSchemaCopy.additionalProperties = additionalProperties;
        }
        return shapeSchemaCopy;
    };
    return rewriteRefs;
};
export function shapeToPropertySchema(projectSchema, shape) {
    const definitions = {};
    const definitionNames = new Set();
    const convert = createShapeToPropertySchema(projectSchema, definitions, definitionNames, {
        useShapeIdDiscriminator: true
    });
    return {
        ...convert(shape),
        definitions
    };
}
/**
 * Currently this is not 100% valid JSON schema, but its output can be used with createSchemaValidator
 */
export function shapeToJsonSchema(projectSchema, shape) {
    const definitions = {};
    const definitionNames = new Set();
    const convert = createShapeToPropertySchema(projectSchema, definitions, definitionNames, {
        useShapeIdDiscriminator: false,
        additionalProperties: false
    });
    return {
        $id: shape.id,
        ...convert(shape),
        definitions
    };
}
export function projectSchemaToJSONSchema(projectSchema) {
    const definitions = {};
    const definitionNames = new Set();
    const convert = createShapeToPropertySchema(projectSchema, definitions, definitionNames, {
        useShapeIdDiscriminator: false
    });
    for (const shape of Object.values(projectSchema.shapes)) {
        definitions[shape.name] = convert(shape);
        definitionNames.add(shape.name);
    }
    return {
        definitions
    };
}
export function getShapeDependencies(projectSchema, shape, predicate) {
    const shapeNames = new Set();
    const findDependencies = (schema) => {
        const shouldFollow = Boolean(!predicate || predicate(schema));
        if (shouldFollow) {
            const refItem = getRef(projectSchema, schema);
            if (refItem) {
                const refShape = refItemToShape(projectSchema, refItem);
                if (refShape && !shapeNames.has(refShape.name)) {
                    shapeNames.add(refShape.name);
                    findDependencies(dereferenceSchema(projectSchema, refShape.schema, ['shapes', refShape.name]));
                }
            }
            else if (isObjectSchema(schema) ||
                isAllOfSchema(schema) ||
                isOneOfSchema(schema) ||
                isExtendsSchema(schema) ||
                isArraySchema(schema)) {
                visitSchemaProperties(schema, [], findDependencies);
            }
            const args = schema['@args'];
            if (args) {
                findDependencies(typeof args === 'string' ? { '@ref': args } : args);
            }
        }
        return shouldFollow;
    };
    findDependencies(shape.schema);
    return [...shapeNames];
}
export function getRelevantShapes(projectSchema, params) {
    const { shapeIds, shapeNames, byId = false, includeDependencies = true } = params;
    if (Boolean(shapeNames?.length) || Boolean(shapeIds?.length)) {
        const ids = new Set(shapeIds);
        const names = new Set(shapeNames);
        const shapes = {};
        for (const shape of Object.values(projectSchema.shapes)) {
            if (ids.has(shape.id) || names.has(shape.name)) {
                const shapeNames = includeDependencies
                    ? [shape.name].concat(getShapeDependencies(projectSchema, shape))
                    : [shape.name];
                for (const shapeName of shapeNames) {
                    const dependentShape = projectSchema.shapes[shapeName];
                    shapes[byId ? dependentShape.id : dependentShape.name] = dependentShape;
                }
            }
        }
        return shapes;
    }
    return {};
}
export function visitSchemaProperties(root, path, callback) {
    const visitEach = (entries, path) => {
        for (const [name, propSchema] of entries) {
            const newPath = [...path, name];
            visit(propSchema, newPath);
        }
    };
    const visit = (schema, path) => {
        if (schema !== root && callback(schema, path) === false) {
            return;
        }
        if (isArraySchema(schema)) {
            visit(schema.items, [...path, 'items']);
        }
        else if (isObjectSchema(schema)) {
            visitEach(Object.entries(schema.properties), [...path, 'properties']);
        }
        else if (isExtendsSchema(schema)) {
            visitEach(Object.entries(schema.extends), [...path, 'extends']);
        }
        else if (isAllOfSchema(schema)) {
            visitEach(Object.entries(schema.allOf), [...path, 'allOf']);
        }
        else if (isOneOfSchema(schema)) {
            visitEach(Object.entries(schema.oneOf), [...path, 'oneOf']);
        }
    };
    visit(root, path);
}
export function visitShapeProperties(shapes, callback) {
    for (const [shapeName, shape] of Object.entries(shapes)) {
        visitSchemaProperties(shape.schema, ['shapes', shapeName, 'schema'], callback);
    }
}
function addRef(refs, ref, predicate) {
    if (ref && (!predicate || predicate(ref))) {
        refs.push(ref);
    }
}
/**
 * Walk through a `ProjectSchemaJSON` and collect all the `ref`s.
 */
export function getAllRefsInShapes(projectSchema, predicate) {
    const items = [];
    const refPropNames = ['$ref', '@ref', '@args', 'args', 'interfaces'];
    visit(projectSchema.shapes, refPropNames, (interfaces, refPath) => {
        const propName = refPath[refPath.length - 1];
        const path = ['shapes', ...refPath];
        if (propName === 'interfaces' && isArray(interfaces)) {
            interfaces.forEach((interfaceRefExpression, index) => {
                addRef(items, {
                    ...parseRef(projectSchema, interfaceRefExpression),
                    path: [...path, index]
                }, predicate);
            });
        }
        // The client sometimes generates invalid schemas, like refs that are
        // undefined. Treat ref as unknown and toss those out.
        // See the `useShapeEditor > can deal with renames test`
        if (!isString(interfaces)) {
            return;
        }
        if (propName === '@ref') {
            addRef(items, { ...parseRef(projectSchema, interfaces), path }, predicate);
        }
        else if (propName === '$ref') {
            addRef(items, { ...refToRefItem(projectSchema, interfaces), path }, predicate);
        }
        else if (propName === '@args' || propName === 'args') {
            addRef(items, { ...parseRef(projectSchema, interfaces), path }, predicate);
        }
    });
    return items;
}
export function getRefsInAgents(context, predicate) {
    const refs = [];
    const agents = getAgentMap(context);
    if (agents) {
        for (const [agentKey, agent] of Object.entries(agents)) {
            const agentPath = [...AGENTS_SCHEMA_PATH, agentKey];
            agent.api.inputs.forEach((input, index) => {
                const ref = {
                    ...parseRef(context, input.args),
                    path: [...agentPath, 'api', 'inputs', index, 'args']
                };
                addRef(refs, ref, predicate);
            });
            for (const [stateKey, state] of Object.entries(agent.states)) {
                const statePath = [...agentPath, 'states', stateKey];
                if (state.execution.type === 'chat') {
                    if (state.execution.artifact) {
                        const ref = {
                            ...parseRef(context, state.execution.artifact),
                            path: [...statePath, 'execution', 'artifact']
                        };
                        addRef(refs, ref, predicate);
                    }
                }
                else if (state.execution.type === 'generate') {
                    if (state.execution.outputShape) {
                        const ref = {
                            ...parseRef(context, state.execution.outputShape),
                            path: [...statePath, 'execution', 'outputShape']
                        };
                        addRef(refs, ref, predicate);
                    }
                }
            }
        }
    }
    return refs;
}
/**
 * Get all refs
 */
export function getAllRefsInQuery(projectSchema, queryPath, query, predicate) {
    const refItems = [];
    const propertyVisitor = (propSchema, propSchemaPath) => {
        addRef(refItems, getRefOrItemsRef(projectSchema, propSchema, propSchemaPath), predicate);
    };
    if (query.args && typeof query.args === 'string') {
        addRef(refItems, getArgsReferenceWithPath(projectSchema, query, queryPath), predicate);
    }
    if (query.args && typeof query.args === 'object') {
        visitSchemaProperties(query.args, [...queryPath, 'args'], propertyVisitor);
    }
    if (query.shape) {
        const { isArray, ref } = parseReturnShape(projectSchema, query.shape);
        if (ref) {
            addRef(refItems, {
                ...ref,
                path: queryPath.concat(isArray ? ['shape', 'items', '@ref'] : ['shape'])
            }, predicate);
        }
    }
    return refItems;
}
/**
 * Collect refs from `queries` or `mutations`.
 */
export function getAllRefsInQueries(projectSchema, rootPropertyName, predicate) {
    const refItems = [];
    const rootPath = [rootPropertyName];
    for (const [queryName, query] of Object.entries(projectSchema[rootPropertyName])) {
        const queryPath = rootPath.concat([queryName]);
        const queryRefs = getAllRefsInQuery(projectSchema, queryPath, query, predicate);
        if (queryRefs.length) {
            refItems.push(...queryRefs);
        }
    }
    return refItems;
}
/**
 * Get all refs in a ProjectSchemaJSON
 * @param predicate return true to include the ref in
 */
export function getAllRefs(projectSchema, predicate) {
    return [
        ...getAllRefsInShapes(projectSchema, predicate),
        ...getAllRefsInQueries(projectSchema, 'queries', predicate),
        ...getAllRefsInQueries(projectSchema, 'mutations', predicate),
        ...getRefsInAgents(projectSchema, predicate)
    ];
}
/**
 * Get all valid refs in a ProjectSchemaJSON
 * @param predicate return true to include the ref in
 */
export function getAllValidRefs(projectSchema, predicate) {
    return getAllRefs(projectSchema, predicate).filter(isValidRefItem(projectSchema));
}
export function splitMapping(mapping) {
    const parts = mapping.split('.');
    return [parts[0], parts.slice(1).join('.')];
}
export function findMapping(schema, predicate) {
    const mapping = schema['@mapping'];
    if (Array.isArray(mapping)) {
        return mapping.find(predicate);
    }
    return mapping && predicate(mapping) ? mapping : undefined;
}
export function getFirstMapping(schema) {
    return Array.isArray(schema['@mapping']) ? schema['@mapping'][0] : schema['@mapping'];
}
/**
 * Find key for use storing data in ShapeDB or API Indexing this allows renaming of properties without db migration
 * Mapping must be using the shapedb service id
 * @param schema
 */
export function getStorageKey(schema) {
    const first = findMapping(schema, (mapping) => mapping.startsWith('shapedb:'));
    return first ? splitMapping(first)[1] : undefined;
}
export function emptySchema(projectId) {
    return {
        projectId,
        defaultLocale: 'en-us',
        locales: ['en-us'],
        apiVersion: '2',
        schemaVersion: CURRENT_SCHEMA_VERSION,
        queries: {},
        mutations: {},
        shapes: {},
        workflows: {},
        forms: {}
    };
}
export function createLayerSchema(partialSchema) {
    return {
        schemaVersion: CURRENT_SCHEMA_VERSION,
        queries: {},
        mutations: {},
        shapes: {},
        ...partialSchema
    };
}
export function getShapesById(projectSchema) {
    return keyBy(Object.values(projectSchema.shapes), 'id');
}
/**
 * Walk through the project schema and return an array of all
 * referenced shape names that are in a service namespace.
 *
 * @param projectSchema A V3 project schema.
 *
 * @returns A `string` array with all the shapes that are in namespaces.
 */
export function getAllNamespaceShapes(projectSchema) {
    const isServiceRef = (refItem) => refItem.layerId !== 'local';
    return uniq(getAllValidRefs(projectSchema, isServiceRef).map((ref) => refItemToNamespacedShapeName(projectSchema, ref)));
}
/**
 * Returns a list of all shape names in a service namespace, excluding any
 * that have been set directly in the project schema
 *
 * @todo It's unsound to discard dependencies in this way, should avoid
 *       collisions with aliases or similar any remote shape that collides,
 *       e.g., Shopify_Product -> Shopify_Product_2
 *
 * @param projectSchema A V3 project schema with remote service configs.
 *
 * @returns A `Set` of shape names.
 */
export function getNamespacedShapeNameList(projectSchema) {
    const requiredShapeNames = new Set();
    // All the shape names
    const existingShapeNames = new Set(Object.keys(projectSchema.shapes));
    // All the referenced shape names with namespaces that belong to a service
    const namespacedShapeNames = new Set(getAllNamespaceShapes(projectSchema));
    // Add all namespaced shapes to the schema if they are not already present,
    // e.g. Shopify_Product
    for (const name of namespacedShapeNames) {
        if (!existingShapeNames.has(name)) {
            requiredShapeNames.add(name);
        }
    }
    return requiredShapeNames;
}
/**
 * Filter the service map to return only services passing the GraphQL Service
 * typeguard.
 *
 * @param serviceMap A service map.
 *
 * @returns A new map with the filtered list of services
 */
export function filterGraphQLServiceMap(serviceMap) {
    const filtered = new Map();
    for (const [serviceKey, serviceConfig] of serviceMap) {
        if (serviceConfig.serviceType === 'graphql') {
            filtered.set(serviceKey, serviceConfig);
        }
    }
    return filtered;
}
/**
 * Creates a schema property list chainable fn, allowing you to manipulate and
 * iterate the properties in a schema.
 */
export const createSchemaPropertyList = curry((context, shapeOrSchema) => {
    const propSchema = isShape(shapeOrSchema) ? shapeOrSchema.schema : shapeOrSchema;
    const schema = dereferenceSchema(context, propSchema, isShape(shapeOrSchema) ? ['shapes', shapeOrSchema.name] : undefined);
    const propertyIterator = new Map(isObjectSchema(schema) ? Object.entries(schema.properties) : undefined);
    const defaultOrderByIteratees = [(n) => n[0]];
    const defaultOrderByOrders = ['asc'];
    const _orderBy = (iteratees = defaultOrderByIteratees, orders = defaultOrderByOrders) => {
        self._propertyIterator = new Map(orderBy(iteratees, orders, Array.from(self._propertyIterator)));
        return self;
    };
    const _sortWith = (comparator) => {
        self._propertyIterator = new Map(Array.from(self._propertyIterator).sort(comparator));
        return self;
    };
    const _sortByNamesWithList = (comparatorArray) => {
        // If no array is provided, the original sort is preserved
        if (comparatorArray) {
            self._propertyIterator = new Map(Array.from(self._propertyIterator)
                .filter(([name]) => comparatorArray.includes(name))
                .sort(([nameA], [nameB]) => {
                const indexA = comparatorArray.indexOf(nameA);
                const indexB = comparatorArray.indexOf(nameB);
                return ((indexA > -1 ? indexA : Number.POSITIVE_INFINITY) - (indexB > -1 ? indexB : Number.POSITIVE_INFINITY));
            }));
        }
        return self;
    };
    const _filterBy = (predicate) => {
        self._propertyIterator = new Map(Array.from(self._propertyIterator).filter(predicate));
        return self;
    };
    const _forEach = (iteratee) => {
        Array.from(self._propertyIterator).forEach(iteratee);
        return self;
    };
    const _getNodes = () => {
        return Array.from(self._propertyIterator);
    };
    const _getObject = () => {
        return Array.from(self._propertyIterator).reduce((curr, node) => Object.assign(curr, { [node[0]]: node[1] }), {});
    };
    const _getNames = () => {
        return Array.from(self._propertyIterator, ([name]) => name);
    };
    const _getValues = () => {
        return Array.from(self._propertyIterator, ([, value]) => value);
    };
    const self = {
        _propertyIterator: propertyIterator,
        orderBy: _orderBy,
        sortWith: _sortWith,
        sortByNamesWithList: _sortByNamesWithList,
        filterBy: _filterBy,
        forEach: _forEach,
        getNodes: _getNodes,
        getObject: _getObject,
        getNames: _getNames,
        getValues: _getValues
    };
    return self;
});
function isShape(shapeOrSchema) {
    return 'schema' in shapeOrSchema;
}
class SchemaPropertyAccessor {
    schema;
    properties;
    entries;
    constructor(context, shapeOrSchema) {
        const propSchema = isShape(shapeOrSchema) ? shapeOrSchema.schema : shapeOrSchema;
        this.schema = dereferenceSchema(context, propSchema, isShape(shapeOrSchema) ? ['shapes', shapeOrSchema.name] : undefined);
        this.entries = isObjectSchema(this.schema) ? Object.entries(this.schema.properties) : [];
        this.properties = new Map(this.entries);
    }
    getValue(propertyName) {
        return this.properties.get(propertyName);
    }
    findValue(predicate) {
        return this.entries.find(predicate)?.[1];
    }
    findName(predicate) {
        return this.entries.find(predicate)?.[0];
    }
    getKey(propertyName) {
        const value = this.properties.get(propertyName);
        if (value) {
            return getStorageKey(value);
        }
    }
}
/**
 * Creates a schema property accessor, allowing you to safely access schema
 * properties.
 */
export const createSchemaPropertyAccessor = curry((context, shapeOrSchema) => {
    return new SchemaPropertyAccessor(context, shapeOrSchema);
});
function findQueryHelper(slice, shapeName, resolver, type) {
    const schemaPath = type === 'query' ? 'queries' : 'mutations';
    return Object.keys(slice[schemaPath]).find((queryName) => {
        const queryOrMutation = slice[schemaPath][queryName];
        return (queryOrMutation.resolver.name === resolver &&
            queryOrMutation.resolver.shapeName === shapeName);
    });
}
function ensureQueryHelper(slice, shapeName, resolver, type) {
    const foundQuery = findQueryHelper(slice, shapeName, resolver, type);
    if (!foundQuery) {
        throw new Error(`Failed to find a "${resolver}" ${type} for shape "${shapeName}"`);
    }
    return foundQuery;
}
/**
 * Find the query in the schema that corresponds to the given shape and resolver.
 */
export const findQueryByShape = (projectSchema, shapeName, resolver) => findQueryHelper(projectSchema, shapeName, resolver, 'query');
/**
 * Find the query in the schema that corresponds to the given shape and resolver.
 */
export const findMutationByShape = (projectSchema, shapeName, resolver) => findQueryHelper(projectSchema, shapeName, resolver, 'mutation');
/**
 * Find the query in the schema that corresponds to the given shape and resolver.
 * Throw an error if that query does not exist
 */
export const ensureQueryByShape = (projectSchema, shapeName, resolver) => ensureQueryHelper(projectSchema, shapeName, resolver, 'query');
/**
 * Find the mutation in the schema that corresponds to the given shape and resolver.
 * Throw an error if that mutation does not exist
 */
export const ensureMutationByShape = (projectSchema, shapeName, resolver) => ensureQueryHelper(projectSchema, shapeName, resolver, 'mutation');
/**
 * Find the query in the schema that corresponds to the given shape and resolver.
 */
export const getDefaultMutationName = (shapeName, resolver) => {
    const action = resolver.substring(resolver.indexOf(':') + 1);
    return `${action}${shapeName}`;
};
export function createShape(name, schema, rest) {
    return {
        name,
        id: name,
        title: name,
        schema,
        ...rest
    };
}
export function queryToPropertySchema(projectSchema, query) {
    let mapping;
    if (isGraphqlResolver(query.resolver)) {
        mapping = serializePropertyRef({
            layerId: query.resolver.service,
            shapeName: 'Query',
            propertyName: query.resolver.fieldName
        });
    }
    return {
        ...returnShapeToSchema(projectSchema, query.shape),
        ...(mapping ? { '@mapping': mapping } : {}),
        '@resolver': query.resolver,
        '@args': query.args
    };
}
export function getQueriesShape(projectSchema, operation) {
    return createShape(pascalCase(operation), {
        type: 'object',
        properties: mapValues(projectSchema[operation === 'query' ? 'queries' : 'mutations'], (query) => queryToPropertySchema(projectSchema, query))
    });
}
/**
 * Return true if `schemaWithRef` is a ref to a shape named `shapeName`
 */
export function schemaMatchesShapeName(workingCopy, propertySchema, shapeName) {
    const arrayOfShapeRef = getRef(workingCopy, propertySchema);
    return Boolean(arrayOfShapeRef && refItemToNamespacedShapeName(workingCopy, arrayOfShapeRef) === shapeName);
}
/**
 * Return true if `shape` has a property that is an array of refs to `shapeName`
 */
export const shapeHasArrayPropOfShape = (projectSchema, shape, shapeName) => {
    if (shape && isObjectSchema(shape.schema)) {
        for (const propName in shape.schema.properties) {
            if (Object.hasOwn(shape.schema.properties, propName)) {
                const propSchema = shape.schema.properties[propName];
                if (propSchema.items &&
                    isRefSchema(propSchema.items) &&
                    schemaMatchesShapeName(projectSchema, propSchema.items, shapeName)) {
                    return true;
                }
            }
        }
    }
    return false;
};
export const getQueryMapName = (type) => {
    return type === 'query' ? 'queries' : 'mutations';
};
