import { deepClone, formatShapeName, isDefined, visit } from '@takeshape/util';
import get from 'lodash/get.js';
import keyBy from 'lodash/keyBy.js';
import { createInputSchema } from "../create-input-schema.js";
import { listTypePrefix } from "../migration/index.js";
import { getFlattenedTemplateShapeName, getRef, getRefShapeName, serializeRef } from "../refs.js";
import { createShape, getShapeDependencies } from "../schema-util.js";
import { isCachedShape } from "../types/index.js";
import { mergeObjectSchemas, mergeSchemaProperties } from "../util/merge.js";
import { getInputShapeName, getMutationInputShapeName } from "./names.js";
import { TemplateVerbs } from "./types.js";
import { getWhereSearchArg } from "./where.js";
export const TSGetArgs = getIDQueryArgs('TSGetArgs');
export const TSGetSingletonArgs = getIDQueryArgs('TSGetSingletonArgs');
export const TSListArgs = getShapeListQueryArgs('TSListArgs', true);
export const TSSearchArgs = getShapeListQueryArgs('TSSearchArgs', false);
export const CreateArgs = getMutationArgs('CreateArgs', TemplateVerbs.Create);
export const UpdateArgs = getMutationArgs('UpdateArgs', TemplateVerbs.Update);
export const DuplicateArgs = getMutationArgs('DuplicateArgs', TemplateVerbs.Duplicate);
export const DeleteArgs = getMutationArgs('DeleteArgs', TemplateVerbs.Delete);
export const PaginatedList = getPaginatedListShape;
export const SearchResults = getSearchResultsShape;
export const CreateResult = getMutationResultType(TemplateVerbs.Create);
export const UpdateResult = getMutationResultType(TemplateVerbs.Update);
export const DuplicateResult = getMutationResultType(TemplateVerbs.Duplicate);
export const DeleteResult = getMutationResultType(TemplateVerbs.Delete);
export const TSSearchSortInput = createShape('TSSearchSortInput', {
    type: 'object',
    properties: {
        field: {
            type: 'string',
            description: 'The field to sort by. Use "_score" to sort by relevance (useful with boost).'
        },
        order: {
            type: 'string',
            description: '"asc" for ascending or "desc" for descending'
        }
    },
    required: ['field', 'order']
});
export const commonSearchProps = {
    terms: { type: 'string' },
    from: {
        type: 'integer',
        description: 'The offset from the first result you want to fetch.'
    },
    size: {
        type: 'integer',
        description: 'The maximum number of items to return.'
    },
    filter: {
        type: 'object',
        description: 'An elasticsearch style filter. Overrides onlyEnabled.'
    },
    sort: {
        type: 'array',
        description: 'An list of fields to sort by.',
        items: {
            '@ref': `local:${TSSearchSortInput.name}`
        }
    }
};
export const localeProps = {
    type: 'object',
    properties: {
        locale: { type: 'string' },
        enableLocaleFallback: { type: 'boolean', default: true }
    }
};
export const termsProps = {
    type: 'object',
    properties: {
        terms: { type: 'string' }
    }
};
export const TSContentStructureInput = createShape('TSContentStructureInput', {
    type: 'object',
    properties: {
        path: {
            type: 'string',
            description: 'A deep path to the array being updated (e.g. a.b[1].c).'
        },
        structure: {
            type: 'array',
            description: 'An array where the indices represent the to index, and the values represent the from index.' +
                'For example to transform ["a","b","c","d"] into ["c","a"], this value would be [2,0].',
            items: {
                type: 'integer'
            }
        }
    },
    required: ['path']
}, { description: 'Describes a structural update to an array of data.' });
export function getPaginatedListShape(_context, shape) {
    const shapeName = `${shape.name}${listTypePrefix}`;
    return {
        shapeName,
        dependencies: {
            [shapeName]: createShape(shapeName, {
                type: 'object',
                properties: {
                    items: { type: 'array', items: { '@ref': `local:${shape.name}` } },
                    total: { type: 'integer' }
                },
                required: ['items', 'total']
            })
        }
    };
}
export function getSearchResultsShape(_context, shape) {
    const shapeName = `${shape.name}SearchResults`;
    return {
        shapeName,
        dependencies: {
            [shapeName]: createShape(shapeName, {
                type: 'object',
                properties: {
                    results: {
                        type: 'array',
                        items: { '@ref': `local:${shape.name}` }
                    },
                    total: { type: 'integer' }
                },
                required: ['results', 'total']
            }, { description: `${shape.name} search results` })
        }
    };
}
const contentStructureFields = {
    type: 'object',
    properties: {
        structure: {
            type: 'array',
            items: {
                '@ref': 'local:TSContentStructureInput'
            }
        }
    }
};
const idSchema = {
    type: 'object',
    properties: {
        _id: { type: 'string' }
    },
    required: ['_id']
};
export function getIDQueryArgs(templateName) {
    return (_context, shape) => {
        const schema = shape.model?.type !== 'single' ? mergeSchemaProperties(idSchema, localeProps) : localeProps;
        const shapeName = getFlattenedTemplateShapeName(shape.name, templateName);
        return {
            shapeName,
            dependencies: {
                [shapeName]: createShape(shapeName, schema)
            }
        };
    };
}
function rewriteRefs(context, shapeSchema, rewriteShapeName) {
    const newSchema = deepClone(shapeSchema);
    visit(newSchema, ['$ref', '@ref'], (_, path) => {
        const parentPath = path.slice(0, -1);
        const value = get(newSchema, parentPath);
        if ((value['@ref'] || value.$ref) && !parentPath.includes('@input') && !parentPath.includes('@output')) {
            const refItem = getRef(context, value);
            if (refItem) {
                value['@ref'] = serializeRef(refItem.layerId === 'local' ? { ...refItem, shapeName: rewriteShapeName(refItem.shapeName) } : refItem);
                if (value.$ref) {
                    delete value.$ref;
                }
            }
        }
    });
    return newSchema;
}
/**
 * Construct shapes for a mutation inputs, given source shape info.
 */
export function getMutationArgs(templateName, verb) {
    const getShapeName = getMutationInputShapeName(verb);
    const addLocale = verb === TemplateVerbs.Update || verb === TemplateVerbs.Duplicate;
    const addStructure = verb === TemplateVerbs.Update;
    const isUpdate = verb === TemplateVerbs.Update || verb === TemplateVerbs.Duplicate;
    return ({ projectSchema }, shape) => {
        const inputName = getShapeName(shape.name);
        const isSingleton = shape.model && shape.model.type === 'single';
        const shouldRequireId = !isSingleton &&
            (verb === TemplateVerbs.Update || verb === TemplateVerbs.Delete || verb === TemplateVerbs.Duplicate);
        let rewrittenSchema;
        let inputShapeSchema;
        if (verb !== TemplateVerbs.Delete) {
            rewrittenSchema = createInputSchema(shape.schema, { isUpdate });
            inputShapeSchema = rewriteRefs(projectSchema, rewrittenSchema, getInputShapeName);
        }
        const inputShape = createShape(inputName, mergeObjectSchemas(shouldRequireId ? idSchema : undefined, inputShapeSchema), { description: `${verb} ${shape.name} input` });
        const argsShapeName = getFlattenedTemplateShapeName(shape.name, templateName);
        const argsShape = createShape(argsShapeName, mergeObjectSchemas({
            type: 'object',
            properties: {
                input: { '@ref': `local:${inputShape.name}` },
                clientMutationId: { type: 'string' }
            },
            required: ['input']
        }, addStructure ? contentStructureFields : undefined, addLocale ? localeProps : undefined));
        return {
            shapeName: argsShapeName,
            dependencies: {
                [inputName]: inputShape,
                [argsShapeName]: argsShape,
                ...(verb !== TemplateVerbs.Delete &&
                    keyBy(getShapeDependencies(projectSchema, 
                    // get the shape dependencies of the shape with the @input swapped
                    createShape(inputShape.name, rewrittenSchema), (propSchema) => {
                        if (propSchema['@resolver']) {
                            return false;
                        }
                        const shapeName = getRefShapeName(projectSchema, propSchema);
                        if (shapeName) {
                            return shapeName !== 'TSRelationship' && !projectSchema.shapes[getInputShapeName(shapeName)];
                        }
                        return true;
                    })
                        .map((shapeName) => {
                        const shape = projectSchema.shapes[shapeName];
                        if (!shape) {
                            return;
                        }
                        // We only allow partial input at the top-level
                        return createShape(getInputShapeName(shapeName), rewriteRefs(projectSchema, createInputSchema(shape.schema), getInputShapeName));
                    })
                        .filter(isDefined), 'name')),
                ...(addStructure ? { TSContentStructureInput } : {})
            }
        };
    };
}
export function getMutationResultType(verb) {
    return (_context, shape) => {
        const shapeName = formatShapeName([verb, shape.name, 'result'], {
            shapeNameIndex: 1
        });
        const result = verb === TemplateVerbs.Delete ? { type: 'boolean' } : { '@ref': `local:${shape.name}` };
        return {
            shapeName,
            dependencies: {
                [shapeName]: createShape(shapeName, {
                    type: 'object',
                    properties: {
                        clientMutationId: { type: 'string' },
                        result
                    }
                })
            }
        };
    };
}
export const commonSearchArgs = {
    type: 'object',
    properties: {
        terms: { type: 'string' },
        from: {
            type: 'integer',
            description: 'The offset from the first result you want to fetch.'
        },
        size: {
            type: 'integer',
            description: 'The maximum number of items to return.'
        },
        filter: {
            type: 'object',
            description: 'An elasticsearch style filter. Overrides onlyEnabled.'
        },
        sort: {
            type: 'array',
            description: 'An list of fields to sort by.',
            items: {
                '@ref': `local:${TSSearchSortInput.name}`
            }
        }
    }
};
const legacyListArgs = {
    type: 'object',
    properties: {
        onlyEnabled: {
            type: 'boolean',
            default: true,
            description: 'Filter out content that is not enabled. Defaults to true.'
        }
    }
};
const shapeFilterProps = {
    type: 'object',
    properties: {
        shapeNames: { type: 'array', items: { type: 'string' } },
        shapeIds: { type: 'array', items: { type: 'string' } },
        contentTypeNames: { type: 'array', items: { type: 'string' } },
        contentTypeIds: { type: 'array', items: { type: 'string' } }
    }
};
export function getShapeListQueryArgs(templateName, legacyArgs) {
    return (context, shape) => {
        const { projectSchema } = context;
        const isAllShapes = shape.name === 'TSSearchable';
        const shapeName = isAllShapes ? templateName : getFlattenedTemplateShapeName(shape.name, templateName);
        const shapes = isAllShapes
            ? Object.values(projectSchema.shapes).filter((s) => s.model !== undefined || isCachedShape(s))
            : [shape];
        const { schema, dependencies } = getWhereSearchArg(projectSchema, shapes);
        return {
            shapeName,
            dependencies: {
                ...dependencies,
                TSSearchSortInput,
                [shapeName]: createShape(shapeName, mergeObjectSchemas(commonSearchArgs, localeProps, legacyArgs ? legacyListArgs : undefined, isAllShapes ? shapeFilterProps : undefined, schema))
            }
        };
    };
}
