"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildChartAsCodeSchema = exports.convertOpenApiToDraft07 = exports.getOutputPath = exports.getSwaggerPath = exports.getRepoRoot = void 0;
const tslib_1 = require("tslib");
const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
const node_path_1 = tslib_1.__importDefault(require("node:path"));
const SCHEMA_REF_PREFIX = '#/components/schemas/';
const DEFS_REF_PREFIX = '#/$defs/';
const CHART_SCHEMA_NAME = 'ChartAsCode';
const getRepoRoot = () => node_path_1.default.resolve(__dirname, '../../../../');
exports.getRepoRoot = getRepoRoot;
const getSwaggerPath = () => node_path_1.default.join((0, exports.getRepoRoot)(), 'packages/backend/src/generated/swagger.json');
exports.getSwaggerPath = getSwaggerPath;
const getOutputPath = () => node_path_1.default.join((0, exports.getRepoRoot)(), 'packages/common/src/schemas/json/chart-as-code-1.0.json');
exports.getOutputPath = getOutputPath;
const toObject = (value) => value && typeof value === 'object' && !Array.isArray(value)
    ? value
    : {};
const unique = (values) => [...new Set(values)];
const mergeObjectSchemas = (left, right) => {
    const merged = { ...left, ...right };
    const leftProperties = toObject(left.properties);
    const rightProperties = toObject(right.properties);
    const hasProperties = Object.keys(leftProperties).length > 0 ||
        Object.keys(rightProperties).length > 0;
    if (hasProperties) {
        merged.properties = { ...leftProperties, ...rightProperties };
    }
    const leftRequired = Array.isArray(left.required)
        ? left.required
        : [];
    const rightRequired = Array.isArray(right.required)
        ? right.required
        : [];
    if (leftRequired.length > 0 || rightRequired.length > 0) {
        merged.required = unique([
            ...leftRequired.map((value) => String(value)),
            ...rightRequired.map((value) => String(value)),
        ]);
    }
    return merged;
};
const resolveComponentRef = (ref, components, depth = 0) => {
    if (depth > 10) {
        throw new Error(`Max $ref resolution depth exceeded at: ${ref}`);
    }
    if (!ref.startsWith(SCHEMA_REF_PREFIX)) {
        throw new Error(`Unsupported $ref format: ${ref}`);
    }
    const schemaName = ref.slice(SCHEMA_REF_PREFIX.length);
    const schema = components[schemaName];
    if (!schema) {
        throw new Error(`Missing schema component: ${schemaName}`);
    }
    // Follow transitive $refs (e.g. Omit<X> → $ref → Pick<X> → properties)
    if (typeof schema.$ref === 'string') {
        return resolveComponentRef(schema.$ref, components, depth + 1);
    }
    return schema;
};
const resolveTopLevelAllOf = (schema, components) => {
    if (!Array.isArray(schema.allOf)) {
        return schema;
    }
    return schema.allOf.reduce((acc, part) => {
        const partObject = part && typeof part === 'object' && !Array.isArray(part)
            ? part
            : {};
        const resolvedPart = partObject.$ref
            ? resolveComponentRef(String(partObject.$ref), components)
            : partObject;
        return mergeObjectSchemas(acc, resolvedPart);
    }, {});
};
const collectComponentRefs = (value, refs = new Set()) => {
    if (Array.isArray(value)) {
        value.forEach((item) => collectComponentRefs(item, refs));
        return refs;
    }
    if (!value || typeof value !== 'object') {
        return refs;
    }
    const objectValue = value;
    const refValue = objectValue.$ref;
    if (typeof refValue === 'string' &&
        refValue.startsWith(SCHEMA_REF_PREFIX)) {
        refs.add(refValue.slice(SCHEMA_REF_PREFIX.length));
    }
    Object.values(objectValue).forEach((child) => collectComponentRefs(child, refs));
    return refs;
};
const rewriteRef = (ref) => ref.startsWith(SCHEMA_REF_PREFIX)
    ? `${DEFS_REF_PREFIX}${ref.slice(SCHEMA_REF_PREFIX.length)}`
    : ref;
const getConstStringValue = (schema, components) => {
    if (typeof schema.const === 'string') {
        return schema.const;
    }
    if (Array.isArray(schema.enum) && schema.enum.length === 1) {
        const [value] = schema.enum;
        return typeof value === 'string' ? value : undefined;
    }
    if (typeof schema.$ref === 'string' &&
        schema.$ref.startsWith(SCHEMA_REF_PREFIX)) {
        const referenced = resolveComponentRef(schema.$ref, components);
        return getConstStringValue(referenced, components);
    }
    return undefined;
};
const withConstType = (variantSchema, typeConst) => {
    const variantObject = toObject(variantSchema);
    const properties = toObject(variantObject.properties);
    if (Object.keys(properties).length === 0) {
        return null;
    }
    const required = Array.isArray(variantObject.required)
        ? variantObject.required.map((value) => String(value))
        : [];
    const existingType = toObject(properties.type);
    return {
        ...variantObject,
        properties: {
            ...properties,
            type: {
                ...(existingType.description
                    ? { description: existingType.description }
                    : {}),
                const: typeConst,
            },
        },
        required: unique(['type', ...required]),
    };
};
const convertOpenApiToDraft07 = (value) => {
    if (Array.isArray(value)) {
        return value.map((item) => (0, exports.convertOpenApiToDraft07)(item));
    }
    if (!value || typeof value !== 'object') {
        return value;
    }
    const source = value;
    const convertedEntries = Object.entries(source).map(([key, child]) => [
        key,
        (0, exports.convertOpenApiToDraft07)(child),
    ]);
    const converted = Object.fromEntries(convertedEntries);
    if (typeof converted.$ref === 'string') {
        converted.$ref = rewriteRef(converted.$ref);
    }
    const { nullable } = converted;
    delete converted.nullable;
    if (nullable !== true) {
        return converted;
    }
    if (typeof converted.type === 'string') {
        converted.type = [converted.type, 'null'];
        return converted;
    }
    if (Array.isArray(converted.type)) {
        const typeValues = converted.type.map((item) => String(item));
        converted.type = unique([...typeValues, 'null']);
        return converted;
    }
    return {
        anyOf: [converted, { type: 'null' }],
    };
};
exports.convertOpenApiToDraft07 = convertOpenApiToDraft07;
const tryBuildDiscriminatedChartConfig = (components) => {
    const chartConfigSchema = components.ChartConfig;
    if (!chartConfigSchema) {
        return null;
    }
    let members = [];
    if (Array.isArray(chartConfigSchema.anyOf)) {
        members = chartConfigSchema.anyOf;
    }
    else if (Array.isArray(chartConfigSchema.oneOf)) {
        members = chartConfigSchema.oneOf;
    }
    if (members.length === 0) {
        return null;
    }
    const transformedMembers = [];
    for (const member of members) {
        const memberObject = member && typeof member === 'object' && !Array.isArray(member)
            ? member
            : {};
        const resolvedMember = memberObject.$ref
            ? resolveComponentRef(String(memberObject.$ref), components)
            : memberObject;
        const flattenedMember = resolveTopLevelAllOf(resolvedMember, components);
        const flattenedProperties = toObject(flattenedMember.properties);
        const typeSchema = toObject(flattenedProperties.type);
        const typeConst = getConstStringValue(typeSchema, components);
        if (!typeConst) {
            return null;
        }
        const convertedMember = (0, exports.convertOpenApiToDraft07)(flattenedMember);
        const typedMember = withConstType(convertedMember, typeConst);
        if (!typedMember) {
            return null;
        }
        transformedMembers.push(typedMember);
    }
    return {
        type: 'object',
        oneOf: transformedMembers,
        discriminator: {
            propertyName: 'type',
        },
    };
};
const sortKeysDeep = (value) => {
    if (Array.isArray(value)) {
        return value.map((item) => sortKeysDeep(item));
    }
    if (!value || typeof value !== 'object') {
        return value;
    }
    const objectValue = value;
    const sortedEntries = Object.entries(objectValue)
        .sort(([left], [right]) => left.localeCompare(right))
        .map(([key, child]) => [key, sortKeysDeep(child)]);
    return Object.fromEntries(sortedEntries);
};
const overlayCompatibilityRules = (schema) => {
    const root = { ...schema };
    const rootProperties = toObject(root.properties);
    if (rootProperties.version) {
        rootProperties.version = {
            ...toObject(rootProperties.version),
            const: 1,
        };
    }
    if (rootProperties.dashboardSlug) {
        const existing = toObject(rootProperties.dashboardSlug);
        rootProperties.dashboardSlug = {
            ...(existing.description
                ? { description: existing.description }
                : {}),
            oneOf: [
                {
                    type: 'string',
                    pattern: '^[a-z0-9-]+$',
                },
                {
                    type: 'null',
                },
            ],
        };
    }
    root.properties = rootProperties;
    root.additionalProperties = false;
    return root;
};
const maybeOverlayDiscriminatedChartConfig = (schema, components) => {
    const root = { ...schema };
    const rootProperties = toObject(root.properties);
    if (!rootProperties.chartConfig) {
        return root;
    }
    const discriminated = tryBuildDiscriminatedChartConfig(components);
    if (!discriminated) {
        return root;
    }
    const existingChartConfig = toObject(rootProperties.chartConfig);
    rootProperties.chartConfig = {
        ...(existingChartConfig.description
            ? { description: existingChartConfig.description }
            : {}),
        ...discriminated,
    };
    root.properties = rootProperties;
    return root;
};
const recoverNestedDescriptions = (schema, components) => {
    const savedChart = components.SavedChart;
    if (!savedChart) {
        return schema;
    }
    const sourceProperties = toObject(savedChart.properties);
    const root = { ...schema };
    const rootProperties = toObject(root.properties);
    for (const [propName, propValue] of Object.entries(rootProperties)) {
        const rootProp = toObject(propValue);
        const sourceProp = toObject(sourceProperties[propName]);
        if (rootProp.properties && sourceProp.properties) {
            const rootSubProperties = toObject(rootProp.properties);
            const sourceSubProperties = toObject(sourceProp.properties);
            let changed = false;
            for (const [subName, subValue] of Object.entries(rootSubProperties)) {
                const rootSub = toObject(subValue);
                const sourceSub = toObject(sourceSubProperties[subName]);
                if (!rootSub.description && sourceSub.description) {
                    rootSubProperties[subName] = {
                        ...rootSub,
                        description: sourceSub.description,
                    };
                    changed = true;
                }
            }
            if (changed) {
                rootProperties[propName] = {
                    ...rootProp,
                    properties: rootSubProperties,
                };
            }
        }
    }
    root.properties = rootProperties;
    return root;
};
const buildChartAsCodeSchema = (swagger) => {
    const components = swagger.components?.schemas;
    if (!components) {
        throw new Error('Missing `components.schemas` in swagger document');
    }
    const chartSchema = components[CHART_SCHEMA_NAME];
    if (!chartSchema) {
        throw new Error(`Missing \`components.schemas.${CHART_SCHEMA_NAME}\` in swagger document`);
    }
    const rootSchema = resolveTopLevelAllOf(chartSchema, components);
    const refsToVisit = [...collectComponentRefs(rootSchema)];
    const visited = new Set();
    while (refsToVisit.length > 0) {
        const schemaName = refsToVisit.pop();
        if (!schemaName || visited.has(schemaName)) {
            // no-op
        }
        else {
            visited.add(schemaName);
            const component = components[schemaName];
            if (!component) {
                throw new Error(`Missing referenced component schema: ${schemaName}`);
            }
            collectComponentRefs(component).forEach((nestedRef) => {
                if (!visited.has(nestedRef))
                    refsToVisit.push(nestedRef);
            });
        }
    }
    const defs = [...visited]
        .sort((left, right) => left.localeCompare(right))
        .reduce((acc, schemaName) => {
        acc[schemaName] = (0, exports.convertOpenApiToDraft07)(components[schemaName]);
        return acc;
    }, {});
    const rootWithDescriptions = recoverNestedDescriptions(rootSchema, components);
    const convertedRoot = (0, exports.convertOpenApiToDraft07)(rootWithDescriptions);
    const rootWithCompatibility = overlayCompatibilityRules(convertedRoot);
    const rootWithChartConfig = maybeOverlayDiscriminatedChartConfig(rootWithCompatibility, components);
    return {
        $schema: 'http://json-schema.org/draft-07/schema#',
        $id: 'https://schemas.lightdash.com/lightdash/chart-as-code.json',
        title: 'Lightdash Chart as Code',
        description: 'Schema for defining Lightdash charts in YAML format for version control',
        ...rootWithChartConfig,
        $defs: defs,
    };
};
exports.buildChartAsCodeSchema = buildChartAsCodeSchema;
const parseSwagger = (content) => JSON.parse(content);
const toStableJson = (value) => `${JSON.stringify(sortKeysDeep(value), null, 4)}\n`;
const run = () => {
    const checkMode = process.argv.includes('--check');
    const swaggerPath = (0, exports.getSwaggerPath)();
    const outputPath = (0, exports.getOutputPath)();
    const swaggerContent = node_fs_1.default.readFileSync(swaggerPath, 'utf8');
    const swagger = parseSwagger(swaggerContent);
    const generatedSchema = (0, exports.buildChartAsCodeSchema)(swagger);
    const nextContent = toStableJson(generatedSchema);
    if (checkMode) {
        const currentContent = node_fs_1.default.readFileSync(outputPath, 'utf8');
        if (currentContent !== nextContent) {
            console.error('chart-as-code schema is out of date. Run `pnpm generate:chart-as-code-schema`.');
            process.exit(1);
        }
        return;
    }
    node_fs_1.default.writeFileSync(outputPath, nextContent, 'utf8');
};
if (require.main === module) {
    run();
}
//# sourceMappingURL=generateChartAsCodeSchema.js.map