import { NotFoundError } from '@takeshape/errors';
import { deepCloneWith, ensureArray, isDefined, isIntegerLike, isIterableObject, isRecord, visit } from '@takeshape/util';
import compact from 'lodash/compact.js';
import assign from 'lodash/fp/assign.js';
import omit from 'lodash/fp/omit.js';
import get from 'lodash/get.js';
import isString from 'lodash/isString.js';
import { isScalar } from "./scalars.js";
import { getServiceNamespaces } from "./services/util.js";
import { isAllOfSchema, isExtendsSchema, isObjectSchema, isOneOfSchema, isRefSchema, isRefSchemaLegacy } from "./types/index.js";
import { mergeDeepObjectSchemas, mergeObjectSchemas } from "./util/merge.js";
import { getShape } from "./util/shapes.js";
export const LOCAL_LAYER_ID = 'local';
const templateShapeRegex = /^(\w+)<([\w:-]+)>$/;
/**
 * Parse a template like `PaginatedList<Post>` and return both the template and the shape name.
 */
export function parseTemplateShape(shapeExpression) {
    const matches = templateShapeRegex.exec(shapeExpression);
    if (matches) {
        return { shapeName: matches[2], template: matches[1] };
    }
    return { shapeName: shapeExpression, template: undefined };
}
export function parseReturnShape(context, shape) {
    if (typeof shape === 'object') {
        if (isRefSchema(shape.items)) {
            const ref = getRef(context, shape.items);
            if (!ref) {
                throw new Error(`Could not parse ${JSON.stringify(shape.items)}: invalid ref`);
            }
            return {
                isArray: true,
                ref,
                shapeName: refItemToNamespacedShapeName(context, ref)
            };
        }
        if (typeof shape.items.type !== 'string') {
            throw new Error(`Could not parse ${JSON.stringify(shape.items)}: invalid type`);
        }
        return { isArray: true, shapeName: shape.items.type };
    }
    const ref = parseRef(context, shape);
    const shapeName = refItemToNamespacedShapeName(context, ref);
    return { isArray: false, shapeName, ref, template: ref.template };
}
/**
 * Convert Query.shape to a PropertySchema
 */
export function returnShapeToSchema(projectSchema, returnShape) {
    if (typeof returnShape === 'string') {
        if (isScalar(returnShape) || returnShape === 'object') {
            return { type: returnShape };
        }
        return {
            '@ref': serializeRef(parseRef(projectSchema, returnShape))
        };
    }
    return returnShape;
}
/**
 * Create a template string like `PaginatedList<Post>` from a template and shape name.
 */
export function createTemplateShapeName(template, shapeName) {
    return `${template}<${shapeName}>`;
}
/**
 * If the string is a template like `PaginatedList<Post>`, return the shape name,
 * in this case `Post`. Otherwise return the input.
 */
export const untemplate = (input) => {
    return parseTemplateShape(input).shapeName;
};
export function getFlattenedTemplateShapeName(shapeName, template) {
    return `${shapeName}${template ?? ''}`;
}
function $refToPath(ref) {
    return ref.substr(2).split('/');
}
function $refToShapeName(ref) {
    return $refToPath(ref)[1];
}
export function refSchemaToPath(context, refSchema) {
    const refItem = getRef(context, refSchema);
    if (refItem === undefined) {
        throw new Error('Invalid ref in refSchemaToPath');
    }
    return refItemToShapePath(context, refItem);
}
/**
 * Safely turn a "template" shape reference (`@args`, `args`, `shape`) into a `RefItem`
 *
 * @param context A partial project schema
 * @param refExpression
 * A ref expression might include "template" name and refer to a local or remote
 * shape, `Foo` or `CreateArgs<Foo>` or `CreateArgs<remote:Foo>`.*
 */
export function parseRef(context, refExpression) {
    const { shapeName, template } = parseTemplateShape(refExpression);
    if (shapeName.includes(':')) {
        return refExpressionToRefItem(shapeName, template);
    }
    return shapeNameToRefItem(context, shapeName, template);
}
export function isRefEqual(context, a, b) {
    const parsedA = typeof a === 'string' ? parseRef(context, a) : a;
    const parsedB = typeof b === 'string' ? parseRef(context, b) : b;
    return (parsedA.shapeName === parsedB.shapeName &&
        parsedA.layerId === parsedB.layerId &&
        parsedA.template === parsedB.template);
}
/**
 * Converts a list of ref expressions into a list of ref items.
 */
export function refExpressionListToRefItemList(context, refExpressionList) {
    return refExpressionList.map((ref) => parseRef(context, ref));
}
/**
 * Sugar for converting a `refExpression` directly into a shape, without the
 * intermediate `refItem`.
 *
 * @param context A partial project schema
 * @param refExpression
 * A ref expression might include "template" name and refer to a local or remote
 * shape, `Foo` or `CreateArgs<Foo>` or `CreateArgs<remote:Foo>`.
 */
export function getShapeByRef(context, refExpression) {
    return refItemToShape(context, parseRef(context, refExpression));
}
/**
 * Provided a service name and a referenced type. Supports 2 or 3 part
 * references, e.g., `shopify:my-store:ProductInput` or `my-store:ProductInput`.
 *
 * @returns
 * A 2 member array, where 0 is the service name, 1 is the referenced type.
 */
function splitRefExpression(ref) {
    const index = ref.lastIndexOf(':');
    const serviceKey = index === -1 ? LOCAL_LAYER_ID : ref.substring(0, index);
    const referencedType = index === -1 ? ref : ref.substring(index + 1);
    return [serviceKey, referencedType];
}
export function isValidServiceId(context, serviceId) {
    return serviceId === LOCAL_LAYER_ID || Boolean(context.services?.[serviceId]);
}
export function isValidRefItem(context) {
    return (refItem) => {
        return isValidServiceId(context, refItem.layerId);
    };
}
/**
 * Parses and returns a typeName, serviceKey, and potentially a namespace for
 * the type.
 */
function refExpressionToRefItem(ref, template) {
    const [serviceKey, typeName] = splitRefExpression(ref);
    return {
        shapeName: typeName,
        layerId: serviceKey,
        template
    };
}
export function refToRefItem(context, $ref, template) {
    return shapeNameToRefItem(context, $refToShapeName($ref), template);
}
/**
 * Parse a name (query, shape, etc) that might have a namespace and find the corresponding service layer id
 */
export function parseName(context, name) {
    const shapeNameParts = name.split('_');
    let layerId;
    if (shapeNameParts.length > 1) {
        const namespaces = getServiceNamespaces(context);
        const maybeNamespace = shapeNameParts[0];
        layerId = namespaces.get(maybeNamespace);
        if (layerId) {
            name = shapeNameParts.slice(1).join('_');
        }
    }
    return {
        name,
        layerId: layerId ?? LOCAL_LAYER_ID
    };
}
function shapeNameToRefItem(context, shapeName, template) {
    const parsed = parseName(context, shapeName);
    return {
        shapeName: parsed.name,
        layerId: parsed.layerId,
        template
    };
}
export function shapeToRefItem(context, shape) {
    return shapeNameToRefItem(context, shape.name);
}
export function shapeToRefString(context, shape) {
    return serializeRef(shapeNameToRefItem(context, shape.name));
}
/**
 * Given a property schema and project schema, finds a `@ref` or `$ref` and
 * returns a `RefItem` suitable for loading a referenced schema...
 */
export function getRef(context, refSchema) {
    if (refSchema['@ref']) {
        // @ref: my-service:ShapeName
        const { shapeName, template } = parseTemplateShape(refSchema['@ref']);
        return refExpressionToRefItem(shapeName, template);
    }
    if (refSchema.$ref) {
        // $ref: #/shapes/[ShapeName OR Namespace_TypeName]/schema
        return refToRefItem(context, refSchema.$ref);
    }
}
export function getRefWithPath(context, refSchema, schemaPath = []) {
    const refItem = getRef(context, refSchema);
    if (refItem) {
        const refType = refSchema['@ref'] ? '@ref' : '$ref';
        return {
            ...refItem,
            path: schemaPath.concat([refType])
        };
    }
}
/**
 * Tests for a `@ref` or `$ref` property directly on the passed schema.
 *
 * @param schema Any schema that might could have a ref property.
 */
export function hasRefProperty(schema) {
    return Boolean(schema['@ref'] ?? schema.$ref);
}
/**
 * From a property that has oneOf or items with oneOf, get all refs
 */
export function getOneOfRefs(context, propertySchema, schemaPath = []) {
    const oneOf = propertySchema.oneOf ?? propertySchema.items?.oneOf;
    const path = propertySchema.oneOf ? schemaPath : [...schemaPath, 'items'];
    if (oneOf) {
        return compact(oneOf.map((one) => {
            return getRefWithPath(context, one, path);
        }));
    }
    return [];
}
/**
 * From a schema with a ref, will return the top-level ref, and potentially
 * a ref item from a nested `items` property.
 */
export function getRefOrItemsRef(context, refSchema, schemaPath = []) {
    const refItem = getRefWithPath(context, refSchema, schemaPath);
    if (refItem) {
        return refItem;
    }
    if (refSchema.items) {
        return getRefOrItemsRef(context, refSchema.items, schemaPath.concat(['items']));
    }
}
/**
 * Turns a `RefItem` into an `@ref`.
 */
export function serializeRef(refItem) {
    const shapeRef = formatShapeRef(refItem.layerId, refItem.shapeName);
    return refItem.template ? createTemplateShapeName(refItem.template, shapeRef) : shapeRef;
}
/**
 * Turns a `RefItem` into a standard shape name, namespacing if present.
 */
export function refItemToNamespacedShapeName(context, refItem) {
    const { shapeName, layerId } = refItem;
    const namespace = getNamespace(context, layerId);
    return namespace ? `${namespace}_${shapeName}` : shapeName;
}
export function refExpressionToNamespacedName(context, refExpression) {
    return refItemToNamespacedShapeName(context, parseRef(context, refExpression));
}
/**
 * Turns a `RefItem` into a path suitable for `lodash.get`.
 */
export function refItemToShapePath(context, refItem) {
    return ['shapes', refItemToNamespacedShapeName(context, refItem)];
}
/**
 * Get a Shape referenced by a `RefItem`.
 */
export function refItemToShape(context, refItem) {
    const shapePath = refItemToShapePath(context, refItem);
    return get(context, shapePath);
}
/**
 * Get all shapes referenced by a RefItem array.
 */
export function refItemListToShapeList(context, refItems) {
    return refItems.map((ref) => refItemToShape(context, ref)).filter(isDefined);
}
/**
 * Get a shape referenced by a `RefItem`, also returning the path to the new shape.
 */
export function refItemToShapeWithPath(context, refItem) {
    const shapePath = refItemToShapePath(context, refItem);
    const shape = get(context, shapePath);
    return shape ? { shape, path: shapePath } : undefined;
}
/**
 * Get a Shape schema referenced by a `RefItem`.
 */
export function refItemToShapeSchema(context, refItem) {
    const shapePath = refItemToShapePath(context, refItem);
    return get(context, [...shapePath, 'schema']);
}
/**
 * Sugar, for when you really just need a string...
 */
export function getRefShapeName(context, refSchema) {
    const refItem = getRef(context, refSchema);
    return refItem ? refItemToNamespacedShapeName(context, refItem) : undefined;
}
/**
 * Sugar, for when you really just need a path...
 */
export function refSchemaToShapeSchemaPath(context, refSchema) {
    const refItem = getRef(context, refSchema);
    return refItem ? [...refItemToShapePath(context, refItem), 'schema'] : undefined;
}
/**
 * Helper fn to omit `ref` props from the target schema, and then extend it with the source schema.
 */
export const omitRefAndExtend = (targetSchema, sourceSchema) => assign(omit(['@ref', '$ref'], targetSchema), sourceSchema);
/**
 * If there is a $ref, this will insert a property schema from the top level
 * in place of the reference.
 */
export function followRef(context, schema, ref) {
    const refItem = ref ?? getRef(context, schema);
    if (refItem) {
        const referencedSchema = refItemToShapeSchema(context, refItem);
        if (referencedSchema) {
            return omitRefAndExtend(schema, referencedSchema);
        }
    }
    return schema;
}
/**
 * Returns whether a schema has refs.
 */
export function hasRef(schema) {
    if (isRefSchema(schema) || isRefSchemaLegacy(schema)) {
        return true;
    }
    if (isAllOfSchema(schema)) {
        return Boolean(schema.allOf.find(hasRef));
    }
    if (isOneOfSchema(schema)) {
        return Boolean(schema.oneOf.find(hasRef));
    }
    return false;
}
/**
 * Returns whether a schema has refs which can be followed.
 */
export function hasResolvableRef(context, schema) {
    if (isRefSchema(schema) || isRefSchemaLegacy(schema)) {
        const ref = getRef(context, schema);
        return Boolean(ref && refItemToShapeSchema(context, ref));
    }
    if (isAllOfSchema(schema)) {
        return Boolean(schema.allOf.find((s) => hasResolvableRef(context, s)));
    }
    if (isOneOfSchema(schema)) {
        return Boolean(schema.oneOf.find((s) => hasResolvableRef(context, s)));
    }
    return false;
}
function isSelfReferential(path, shapeName) {
    if (path[0] !== 'shapes' || path[1] !== shapeName) {
        return false;
    }
    const index = path.findIndex((part) => part === 'allOf' || part === 'extends');
    return index !== -1 && isIntegerLike(path[Number(index) + 1]);
}
/**
 * Given a schema, will resolve any `allOf` or `ref` objects. This function has
 * to fully resolve all refs, because attempting to extend an object schema with
 * a ref property would create an invalid schema, potentially containing both
 * a ref and properties.
 *
 * If it can't create a valid schema it returns the input.
 */
export function dereferenceSchema(context, shapeOrFieldSchema, schemaPath = []) {
    const resolveAll = (propertySchema, resolvePath) => {
        try {
            if (isAllOfSchema(propertySchema)) {
                return merge(propertySchema.allOf, 'allOf', resolvePath);
            }
            if (isExtendsSchema(propertySchema)) {
                return merge(propertySchema.extends, 'extends', resolvePath);
            }
            if (isRefSchema(propertySchema)) {
                const refItem = getRefWithPath(context, propertySchema);
                if (!refItem) {
                    throw new Error('ref could not be parsed');
                }
                const shapeWithPath = refItemToShapeWithPath(context, refItem);
                if (shapeWithPath && !isSelfReferential(resolvePath, shapeWithPath.shape.name)) {
                    /**
                     * Pass the new shape's path rather than the parent's path, which
                     * will have been truncated to something opaque, like `@ref`.
                     */
                    return resolveAll(omitRefAndExtend(propertySchema, shapeWithPath.shape.schema), shapeWithPath.path);
                }
                return;
            }
            return propertySchema;
        }
        catch (err) {
            throw new Error(`error at '${resolvePath.join('.')}', ${err.message}`);
        }
    };
    const merge = (schemas, propName, path) => {
        const resolvedSchemas = schemas.map((schema, index) => {
            const resolved = resolveAll(schema, [...path, propName, index]);
            return resolved && isObjectSchema(resolved) ? resolved : undefined;
        });
        return (propName === 'allOf' ? mergeDeepObjectSchemas : mergeObjectSchemas)(...resolvedSchemas);
    };
    return resolveAll(shapeOrFieldSchema, schemaPath) ?? shapeOrFieldSchema;
}
export function dereferenceObjectSchema(context, shapeOrFieldSchema, schemaPath = []) {
    const schema = dereferenceSchema(context, shapeOrFieldSchema, schemaPath);
    if (!isObjectSchema(schema)) {
        throw new Error(`provided schema at '${schemaPath.join('.')}' could not be resolved to an object schema`);
    }
    return schema;
}
function getNamespace(context, layerId) {
    return context.services?.[layerId]?.namespace;
}
export function createGetNamespace(context) {
    return (layerId) => getNamespace(context, layerId);
}
export function parsePropertyRef(refStr) {
    const [layerId, coordinate] = splitRefExpression(refStr);
    const coordinateParts = coordinate.split('.');
    if (coordinateParts.length === 1 && coordinateParts[0]) {
        return { layerId, shapeName: 'Query', propertyName: coordinateParts[0] };
    }
    if (coordinateParts.length === 2) {
        return {
            layerId,
            shapeName: coordinateParts[0],
            propertyName: coordinateParts[1]
        };
    }
}
export function serializePropertyRef({ shapeName, propertyName, layerId }) {
    return formatShapeRef(layerId, `${shapeName}.${propertyName}`);
}
export function normalizePropertyRef(refStr) {
    const parsed = parsePropertyRef(refStr);
    return parsed ? serializePropertyRef(parsed) : refStr;
}
export function propertyRefToShapeRef(propertyRef) {
    const parsed = parsePropertyRef(propertyRef);
    return parsed && formatShapeRef(parsed.layerId, parsed.shapeName);
}
export function mappingToShapeRefs(prop) {
    const mappings = prop['@mapping'];
    if (mappings) {
        const result = [];
        for (const mapping of ensureArray(mappings)) {
            const shapeRef = propertyRefToShapeRef(mapping);
            if (shapeRef) {
                result.push(shapeRef);
            }
        }
        return result;
    }
    return [];
}
export function formatShapeRef(layerId, shapeName) {
    return layerId === LOCAL_LAYER_ID ? shapeName : `${layerId}:${shapeName}`;
}
export function applyNamespace(namespace, name) {
    return namespace ? `${namespace}_${name}` : name;
}
export function propertyRefItemToResolverPath(getNamespace, item) {
    const path = propertyRefItemToPath(getNamespace, item);
    return path.concat(path[0] === 'shapes' ? '@resolver' : 'resolver');
}
export function propertyRefItemToArgsPath(getNamespace, item) {
    const path = propertyRefItemToPath(getNamespace, item);
    return path.concat(path[0] === 'shapes' ? '@args' : 'args');
}
export function propertyRefItemToPath(getNamespace, item) {
    const { shapeName, layerId, propertyName } = item;
    const namespace = getNamespace(layerId);
    if (shapeName === 'Query' || shapeName === 'Mutation') {
        return [shapeName === 'Query' ? 'queries' : 'mutations', applyNamespace(namespace, propertyName)];
    }
    return ['shapes', applyNamespace(namespace, shapeName), 'schema', 'properties', propertyName];
}
export function propertyRefToArgs(projectSchema, ref) {
    const parsed = typeof ref === 'object' ? ref : parsePropertyRef(ref);
    if (parsed) {
        const path = propertyRefItemToArgsPath(createGetNamespace(projectSchema), parsed);
        return get(projectSchema, path);
    }
}
/**
 * Get a ref to the given shape.
 */
export function normalizeRefExpression(context, refExpression) {
    return serializeRef(parseRef(context, refExpression));
}
export function normalizeLocalRefString(ref) {
    return ref.startsWith('local:') ? ref.substring(6) : ref;
}
export function getAllPropertyRefs(projectSchema) {
    const result = [];
    const parentIsResolver = (path, resolverName) => {
        const parent = get(projectSchema, path.slice(0, -1));
        return (isRecord(parent) &&
            (Array.isArray(resolverName) ? resolverName.some((name) => name === parent.name) : parent.name === resolverName));
    };
    const parentIsGenerateState = (path) => {
        if (path[0] !== 'agents' || path[path.length - 2] !== 'execution') {
            return false;
        }
        const parent = get(projectSchema, path.slice(0, -1));
        return isRecord(parent) && (parent.type === 'generate' || parent.type === 'chat');
    };
    const pushRef = (refStr) => {
        const parsed = parsePropertyRef(refStr);
        if (parsed) {
            result.push(parsed);
        }
    };
    visit(projectSchema, ['to', 'tools'], (value, path) => {
        const propName = path[path.length - 1];
        if (propName === 'to' && isString(value) && parentIsResolver(path, 'delegate')) {
            pushRef(value);
        }
        else if (propName === 'tools') {
            if (Array.isArray(value) &&
                (parentIsGenerateState(path) || parentIsResolver(path, ['ai:generateText', 'ai:chat']))) {
                const tools = value;
                for (const tool of tools) {
                    pushRef(getToolRef(tool));
                }
            }
            else if (isRecord(value) && (path[0] === 'agents' || path[0] === 'mcp')) {
                for (const tool of Object.values(value)) {
                    pushRef(tool.ref);
                }
            }
        }
    });
    return result;
}
export function getQuery(projectSchema, queryRef) {
    const propertyRef = parsePropertyRef(queryRef);
    if (!propertyRef || (propertyRef.shapeName !== 'Query' && propertyRef.shapeName !== 'Mutation')) {
        throw new Error(`Invalid query ref "${queryRef}"`);
    }
    const path = propertyRefItemToPath(createGetNamespace(projectSchema), propertyRef);
    const query = get(projectSchema, path);
    return query ? { name: path[path.length - 1], query, ref: propertyRef } : undefined;
}
export function ensureQuery(projectSchema, queryRef) {
    const query = getQuery(projectSchema, queryRef);
    if (!query) {
        throw new NotFoundError(`Query "${queryRef}" not found`);
    }
    return query;
}
export function normalizeRefs(projectSchema, property) {
    return deepCloneWith(property ?? projectSchema, (value, _key, _parent, clone) => {
        if (isIterableObject(value)) {
            const ref = getRef(projectSchema, value);
            if (ref) {
                const { $ref, '@output': output, '@input': input, ...newRefSchema } = value;
                newRefSchema['@ref'] = serializeRef(ref);
                if (output) {
                    newRefSchema['@output'] = clone(output, '@output', value);
                }
                if (input) {
                    newRefSchema['@input'] = clone(input, '@input', value);
                }
                return newRefSchema;
            }
        }
    });
}
/**
 * Get a ShapeJSON from a RefItem, with the schema dereferenced.
 */
export function getReferencedShape(projectSchema, refItem, namespace) {
    const shape = getShape(projectSchema, applyNamespace(namespace ?? getNamespace(projectSchema, refItem.layerId), refItem.shapeName));
    if (shape) {
        return {
            ...shape,
            schema: dereferenceSchema(projectSchema, shape.schema, ['shapes', shape.name])
        };
    }
}
/**
 * Get a PropertySchema from a RefItem.
 */
export function getReferencedShapeProperty(projectSchema, refItem, namespace) {
    const shape = getReferencedShape(projectSchema, refItem, namespace ?? getNamespace(projectSchema, refItem.layerId));
    if (shape && isObjectSchema(shape.schema)) {
        return shape.schema.properties[refItem.propertyName];
    }
}
export function getToolRef(tool) {
    return typeof tool === 'string' ? tool : tool.ref;
}
