"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDashboardAsCodeSchema = 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 DASHBOARD_SCHEMA_NAME = 'DashboardAsCode';
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/dashboard-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}`);
    }
    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 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 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 resolveDefRef = (ref, defs, depth = 0) => {
    if (depth > 10) {
        throw new Error(`Max $defs $ref resolution depth exceeded at: ${ref}`);
    }
    if (!ref.startsWith(DEFS_REF_PREFIX)) {
        throw new Error(`Unsupported $defs $ref format: ${ref}`);
    }
    const schemaName = ref.slice(DEFS_REF_PREFIX.length);
    const schema = defs[schemaName];
    if (!schema) {
        throw new Error(`Missing $defs schema: ${schemaName}`);
    }
    if (typeof schema.$ref === 'string') {
        return resolveDefRef(schema.$ref, defs, depth + 1);
    }
    return schema;
};
const flattenObjectSchema = (schema, defs) => {
    const resolvedSchema = typeof schema.$ref === 'string'
        ? resolveDefRef(schema.$ref, defs)
        : schema;
    if (!Array.isArray(resolvedSchema.allOf)) {
        return resolvedSchema;
    }
    return resolvedSchema.allOf.reduce((acc, part) => {
        const partObject = part && typeof part === 'object' && !Array.isArray(part)
            ? part
            : {};
        const resolvedPart = typeof partObject.$ref === 'string'
            ? resolveDefRef(partObject.$ref, defs)
            : partObject;
        return mergeObjectSchemas(acc, flattenObjectSchema(resolvedPart, defs));
    }, {});
};
const applyDashboardTileCompatibilityRules = (defs) => {
    const nextDefs = { ...defs };
    const tileUnion = nextDefs.DashboardTileAsCode;
    if (!tileUnion || !Array.isArray(tileUnion.anyOf)) {
        return nextDefs;
    }
    tileUnion.anyOf.forEach((member) => {
        const memberObject = member && typeof member === 'object' && !Array.isArray(member)
            ? member
            : {};
        if (typeof memberObject.$ref !== 'string') {
            return;
        }
        const schemaName = memberObject.$ref.slice(DEFS_REF_PREFIX.length);
        const variant = nextDefs[schemaName];
        if (!variant) {
            return;
        }
        const flattenedVariant = flattenObjectSchema(variant, nextDefs);
        const variantProperties = toObject(flattenedVariant.properties);
        const tilePropertiesSchema = flattenObjectSchema(toObject(variantProperties.properties), nextDefs);
        tilePropertiesSchema.additionalProperties = false;
        variantProperties.properties = tilePropertiesSchema;
        nextDefs[schemaName] = {
            ...flattenedVariant,
            additionalProperties: false,
            properties: variantProperties,
        };
    });
    return nextDefs;
};
const applyDashboardTabCompatibilityRules = (defs) => {
    const nextDefs = { ...defs };
    ['DashboardTab', 'DashboardTabAsCode'].forEach((schemaName) => {
        const dashboardTab = nextDefs[schemaName];
        if (!dashboardTab) {
            return;
        }
        nextDefs[schemaName] = {
            ...dashboardTab,
            additionalProperties: false,
        };
    });
    return nextDefs;
};
const overlayCompatibilityRules = (schema, defs) => {
    const root = { ...schema };
    const rootProperties = toObject(root.properties);
    if (rootProperties.version) {
        rootProperties.version = {
            ...toObject(rootProperties.version),
            const: 1,
        };
    }
    root.properties = rootProperties;
    root.additionalProperties = false;
    const defsWithStrictTiles = applyDashboardTileCompatibilityRules(defs);
    const defsWithCompatibility = applyDashboardTabCompatibilityRules(defsWithStrictTiles);
    return { schema: root, defs: defsWithCompatibility };
};
const buildDashboardAsCodeSchema = (swagger) => {
    const components = swagger.components?.schemas;
    if (!components) {
        throw new Error('Missing `components.schemas` in swagger document');
    }
    const dashboardSchema = components[DASHBOARD_SCHEMA_NAME];
    if (!dashboardSchema) {
        throw new Error(`Missing \`components.schemas.${DASHBOARD_SCHEMA_NAME}\` in swagger document`);
    }
    const rootSchema = resolveTopLevelAllOf(dashboardSchema, components);
    const refsToVisit = [...collectComponentRefs(rootSchema)];
    const visited = new Set();
    while (refsToVisit.length > 0) {
        const schemaName = refsToVisit.pop();
        if (!schemaName || visited.has(schemaName)) {
            // eslint-disable-next-line no-continue
            continue;
        }
        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 convertedRoot = (0, exports.convertOpenApiToDraft07)(rootSchema);
    const { schema: rootWithCompatibility, defs: defsWithCompatibility } = overlayCompatibilityRules(convertedRoot, defs);
    return {
        $schema: 'http://json-schema.org/draft-07/schema#',
        $id: 'https://schemas.lightdash.com/lightdash/dashboard-as-code.json',
        title: 'Lightdash Dashboard as Code',
        description: 'Schema for defining Lightdash dashboards in YAML format for version control',
        ...rootWithCompatibility,
        $defs: defsWithCompatibility,
    };
};
exports.buildDashboardAsCodeSchema = buildDashboardAsCodeSchema;
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.buildDashboardAsCodeSchema)(swagger);
    const nextContent = toStableJson(generatedSchema);
    if (checkMode) {
        const currentContent = node_fs_1.default.readFileSync(outputPath, 'utf8');
        const normalizedCurrentContent = toStableJson(JSON.parse(currentContent));
        if (normalizedCurrentContent !== nextContent) {
            console.error('dashboard-as-code schema is out of date. Run `pnpm generate:dashboard-as-code-schema`.');
            process.exit(1);
        }
        return;
    }
    node_fs_1.default.writeFileSync(outputPath, nextContent, 'utf8');
};
if (require.main === module) {
    run();
}
//# sourceMappingURL=generateDashboardAsCodeSchema.js.map