/*
 * Copyright (c) 2025, Salesforce, Inc.
 * SPDX-License-Identifier: Apache-2
 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
 */
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { Validator } from 'jsonschema';
// Resolve the content-schemas data directory from the package root
const require = createRequire(import.meta.url);
const packageRoot = path.dirname(require.resolve('@salesforce/b2c-tooling-sdk/package.json'));
export const CONTENT_SCHEMAS_DIR = path.join(packageRoot, 'data/content-schemas');
/** Top-level schema types that users validate files against. */
export const CONTENT_SCHEMA_TYPES = [
    'pagetype',
    'componenttype',
    'aspecttype',
    'cmsrecord',
    'customeditortype',
    'contentassetpageconfig',
    'contentassetcomponentconfig',
    'contentassetstructuredcontentdata',
    'image',
];
/**
 * Error thrown when the schema type cannot be determined from the file path or data.
 */
export class MetaDefinitionDetectionError extends Error {
    constructor(message) {
        super(message ??
            `Unable to detect meta definition type. Use --type to specify one of: ${CONTENT_SCHEMA_TYPES.join(', ')}`);
        this.name = 'MetaDefinitionDetectionError';
    }
}
// Lazy-loaded validator singleton
let validatorInstance = null;
function getValidator() {
    if (validatorInstance)
        return validatorInstance;
    validatorInstance = new Validator();
    // Load all schemas so $ref resolution works across files
    for (const file of fs.readdirSync(CONTENT_SCHEMAS_DIR)) {
        if (file.endsWith('.json')) {
            const schemaPath = path.join(CONTENT_SCHEMAS_DIR, file);
            const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
            // Register with /<filename> URI so relative $ref like "common.json#/definitions/name" resolves
            // (jsonschema resolves relative refs against the parent schema's URI)
            validatorInstance.addSchema(schema, `/${file}`);
        }
    }
    return validatorInstance;
}
/**
 * Detect the metadefinition schema type from a file path using Page Designer conventions.
 *
 * - `experience/pages/` → `pagetype`
 * - `experience/components/` → `componenttype`
 */
export function detectTypeFromPath(filePath) {
    const normalized = filePath.replace(/\\/g, '/');
    if (/experience\/pages\//.test(normalized))
        return 'pagetype';
    if (/experience\/components\//.test(normalized))
        return 'componenttype';
    return null;
}
/**
 * Detect the metadefinition schema type from the data's top-level properties.
 */
export function detectTypeFromData(data) {
    const keys = new Set(Object.keys(data));
    if (keys.has('region_definitions')) {
        return keys.has('group') ? 'componenttype' : 'pagetype';
    }
    if (keys.has('supported_object_types'))
        return 'aspecttype';
    if (keys.has('path'))
        return 'image';
    if (keys.has('resources'))
        return 'customeditortype';
    if (keys.has('data_binding'))
        return 'contentassetcomponentconfig';
    if (keys.has('visibility') && !keys.has('data_binding'))
        return 'contentassetpageconfig';
    if (keys.has('attributes') && keys.has('type'))
        return 'cmsrecord';
    return null;
}
/**
 * Detect the metadefinition schema type using a 3-tier strategy:
 * 1. File path convention (if filePath provided)
 * 2. Property heuristics from data
 * 3. Returns null if neither works (caller should use explicit --type or throw)
 */
export function detectMetaDefinitionType(data, filePath) {
    if (filePath) {
        const fromPath = detectTypeFromPath(filePath);
        if (fromPath)
            return fromPath;
    }
    return detectTypeFromData(data);
}
function formatAnyOfError(err) {
    const schema = err.schema;
    if (!schema?.anyOf || !Array.isArray(schema.anyOf))
        return err.message;
    const branches = schema.anyOf;
    const descriptions = [];
    for (const branch of branches) {
        if (branch.required && Array.isArray(branch.required)) {
            const not = branch.not;
            if (not?.required && Array.isArray(not.required)) {
                descriptions.push(`"${branch.required.join('", "')}" (without "${not.required.join('", "')}")`);
            }
            else {
                descriptions.push(`requires "${branch.required.join('", "')}"`);
            }
        }
        else if (branch.not) {
            const notSchema = branch.not;
            if (notSchema.required && Array.isArray(notSchema.required)) {
                descriptions.push(`must not have "${notSchema.required.join('", "')}"`);
            }
        }
    }
    if (descriptions.length > 0) {
        return `must satisfy one of: ${descriptions.join('; or ')}`;
    }
    return err.message;
}
function mapErrors(result) {
    const meaningful = result.errors.filter((err) => err.name !== 'anyOf' && err.name !== 'not' && err.name !== 'allOf');
    if (meaningful.length > 0)
        return meaningful.map(toValidationError);
    const anyOfErrors = result.errors.filter((err) => err.name === 'anyOf');
    if (anyOfErrors.length > 0) {
        return anyOfErrors.map((err) => ({
            path: err.property.replace(/^instance/, '') || '/',
            message: formatAnyOfError(err),
            property: err.property,
        }));
    }
    return result.errors.map(toValidationError);
}
function toValidationError(err) {
    return {
        path: err.property.replace(/^instance/, '') || '/',
        message: err.message,
        property: err.property,
    };
}
/**
 * Validate a parsed JSON object against a content metadefinition schema.
 *
 * @throws {MetaDefinitionDetectionError} When the schema type cannot be detected and no explicit type is provided.
 */
export function validateMetaDefinition(data, options) {
    const schemaType = options?.type ?? detectMetaDefinitionType(data);
    if (!schemaType) {
        throw new MetaDefinitionDetectionError();
    }
    const validator = getValidator();
    const result = validator.validate(data, { $ref: `/${schemaType}.json` }, { nestedErrors: true });
    return {
        valid: result.valid,
        schemaType,
        errors: mapErrors(result),
    };
}
/**
 * Validate a JSON metadefinition file against a content schema.
 *
 * Uses file path conventions for type detection before falling back to property heuristics.
 *
 * @throws {MetaDefinitionDetectionError} When the schema type cannot be detected and no explicit type is provided.
 */
export function validateMetaDefinitionFile(filePath, options) {
    const absolutePath = path.resolve(filePath);
    const content = fs.readFileSync(absolutePath, 'utf-8');
    let data;
    try {
        data = JSON.parse(content);
    }
    catch (err) {
        return {
            valid: false,
            schemaType: options?.type ?? 'unknown',
            errors: [
                {
                    path: '/',
                    message: `Invalid JSON: ${err.message}`,
                    property: 'instance',
                },
            ],
            filePath: absolutePath,
        };
    }
    const schemaType = options?.type ?? detectMetaDefinitionType(data, absolutePath);
    if (!schemaType) {
        throw new MetaDefinitionDetectionError();
    }
    const validator = getValidator();
    const result = validator.validate(data, { $ref: `${schemaType}.json` }, { nestedErrors: true });
    return {
        valid: result.valid,
        schemaType,
        errors: mapErrors(result),
        filePath: absolutePath,
    };
}
//# sourceMappingURL=validate.js.map