import fs from "fs";
import Ajv from "ajv";
import path from "path";
import { fileURLToPath } from "url";
import geComponentTokenSchema from "./component-token-schema.js";
import getGlobalTokenSchema from "./global-token-schema.js";
import validateTokenValue, { isTokenReference } from "./validate-value.js";
import { VALIDATION_RULES, ERROR_TYPES, TOKEN_TYPES, ALLOWED_PROPERTIES_BY_CONTEXT, VALIDATION_SCOPE, VALUE_RELATED_KEYWORDS, PLATFORM, FOLDER_TYPE } from "./constants.js";
//Maps each token type to its validation schema and the rules enabled during validation.
const TOKEN_TYPE_CONFIG = {
    [TOKEN_TYPES.GLOBAL]: {
        getSchema: () => getGlobalTokenSchema(),
        resolveRules: () => ({
            [VALIDATION_RULES.STRUCTURE]: false,
            [VALIDATION_RULES.METADATA]: false,
            [VALIDATION_RULES.REFERENCES]: true,
            [VALIDATION_RULES.OVERRIDES]: false,
            [VALIDATION_RULES.VALUES]: true
        })
    },
    [TOKEN_TYPES.COMPONENT]: {
        getSchema: (enabledRules) => geComponentTokenSchema(enabledRules),
        resolveRules: (validationOptions) => resolveEnabledValidationRules(validationOptions)
    }
};
/*
 * Normalizes and stores a validation error.
 * Inputs:
 * - errorType: category of validation error
 * - fieldName: dot-separated path of the invalid field
 * - message: human-readable error message
 * - fileName: name of the file being validated (optional)
 * - allowedValues: list of allowed values for the field (optional)
 * Output: - Adds a normalized error object to the internal validation error list
 */
const pushError = ({ errorType, fieldName, message, fileName, allowedValues, actualValue }, validationErrors) => {
    const error = { errorType, fieldName, message };
    if (allowedValues)
        error.allowedValues = allowedValues;
    if (fileName)
        error.fileName = fileName;
    if (actualValue !== undefined)
        error.actualValue = actualValue;
    validationErrors.push(error);
};
/*
 * Validates that leaf token nodes define a valid string value.
 * Inputs:
 * - _schema: AJV keyword schema value (not used)
 * - node: token node being validated
 * - ctx: AJV validation context containing the instance path
 * Output:
 * - Returns true if node is valid or not applicable
 * - Returns false and reports validation errors if leaf token is invalid
 */
function leafMustHaveValueValidate(_schema, node, ctx) {
    // Skip meta subtree
    if (ctx.instancePath?.includes("/meta") ||
        ctx.instancePath?.includes("/attributes") ||
        ctx.instancePath?.endsWith("/type")) {
        return true;
    }
    // Primitive string leaf
    if (typeof node === "string") {
        leafMustHaveValueValidate.errors = [
            {
                keyword: "leafMustHaveValue",
                message: `Invalid token definition.\n` +
                    `Leaf tokens must be objects with a "value" property.\n\n` +
                    `Expected format: { "value": "16px" }`
            }
        ];
        return false;
    }
    // null node
    if (node === null) {
        leafMustHaveValueValidate.errors = [
            {
                keyword: "leafMustHaveValue",
                message: `Invalid token definition.\n` +
                    `Token cannot be null.\n\n` +
                    `Expected format: { "value": "16px" }`
            }
        ];
        return false;
    }
    //Ignore meta keys when deciding leaf
    const TOKEN_META_KEYS = new Set(["value", "type", "attributes", "meta"]);
    const childKeys = Object.keys(node).filter(k => !TOKEN_META_KEYS.has(k));
    const hasNestedTokenChildren = childKeys.length > 0 && childKeys.some(k => node[k] && typeof node[k] === "object");
    const hasValue = "value" in node;
    //nodes with "value" cannot have nested token children
    if (hasValue && hasNestedTokenChildren) {
        leafMustHaveValueValidate.errors = [
            {
                keyword: "leafMustHaveValue",
                message: `Invalid token structure.\n` +
                    `A token that defines "value" cannot contain nested token objects.\n` +
                    `Move nested tokens to a separate level.`
            }
        ];
        return false;
    }
    // Object leaf without value
    if (!hasNestedTokenChildren && !hasValue) {
        leafMustHaveValueValidate.errors = [
            {
                keyword: "leafMustHaveValue",
                message: `Invalid token definition.\n` +
                    `Leaf tokens must be objects with a "value" property.\n` +
                    `Expected format: { "value": "16px" }`
            }
        ];
        return false;
    }
    // Invalid string literals
    const INVALID_STRING_LITERALS = new Set(["null", "undefined", "nan"]);
    if (typeof node.value === "string" &&
        INVALID_STRING_LITERALS.has(node.value.trim().toLowerCase())) {
        leafMustHaveValueValidate.errors = [
            {
                keyword: "leafMustHaveValue",
                message: `Invalid token value.\n` +
                    `"${node.value}" is not allowed as a token value.`
            }
        ];
        return false;
    }
    return true;
}
/*
 * Resolves the token subtype from foundationMappingRegistry for an appearances override token.
 * Translates the AJV instancePath into a mapping registry lookup by stripping the appearances
 * hierarchy (appearances/<name>/mapping | variantGroups/<group>/<variant> | states/<state>)
 * and any embedded states segments from the remaining token path.
 *
 * Inputs:
 * - instancePath: AJV instance path of the token node (e.g. /btn/appearances/filled/mapping/border/color)
 * - foundationMappingRegistry: registry built from foundation component mappings
 * Output:
 * - Returns the subtype string if found, or null
 */
function resolveSubtypeFromMapping(instancePath, foundationMappingRegistry) {
    const segments = instancePath.replace(/^\//, "").split("/").filter(Boolean);
    if (!segments.length)
        return null;
    const componentName = segments[0];
    const mappingRoot = foundationMappingRegistry[componentName];
    if (!mappingRoot)
        return null;
    const appearancesIdx = segments.indexOf("appearances");
    if (appearancesIdx === -1)
        return null;
    // Start after: appearances/<appearanceName>
    let idx = appearancesIdx + 2;
    // Skip the override context prefix
    if (segments[idx] === "mapping") {
        idx += 1;
    }
    else if (segments[idx] === "variantGroups") {
        idx += 3; // variantGroups/<group>/<variant>
    }
    else if (segments[idx] === "states") {
        idx += 2; // states/<stateName>
        if (segments[idx] === "mapping")
            idx += 1; // strip trailing mapping if present
    }
    // Build token path, skipping any embedded states/<stateName> segments
    const tokenPath = [];
    while (idx < segments.length) {
        if (segments[idx] === "states") {
            idx += 2;
        }
        else {
            tokenPath.push(segments[idx]);
            idx++;
        }
    }
    // Traverse the mapping structure
    let node = mappingRoot;
    for (const key of tokenPath) {
        if (!node || typeof node !== "object")
            return null;
        node = node[key];
    }
    return node?.__subtype ?? null;
}
/*
 * Validates actual token values against platform/subtype rules.
 * For tokens in the `appearances` section that lack attributes.subtype, the subtype
 * is resolved from foundationMappingRegistry. Reports an error if the subtype cannot
 * be determined for an appearances override.
 *
 * Inputs:
 * - _schema: AJV keyword schema value (not used)
 * - node: token node being validated
 * - ctx: AJV validation context (provides instancePath)
 * Output:
 * - Returns true if not a leaf, rule disabled, or value is valid
 * - Returns false and reports validation errors if value is invalid or subtype is unresolvable
 */
function tokenValueContentValidate(_schema, node, _parentSchema, ctx) {
    const { enabledRules, platform, foundationMappingRegistry } = this.context;
    tokenValueContentValidate.errors = null;
    // Only validate leaf tokens (objects that define a value)
    if (!node || typeof node !== "object" || !("value" in node))
        return true;
    // Rule disabled
    if (!enabledRules?.[VALIDATION_RULES.VALUES])
        return true;
    let subtype = node.attributes?.subtype;
    if (!subtype) {
        const instancePath = ctx.instancePath ?? "";
        if (instancePath.includes("/appearances/")) {
            // For appearance overrides, resolve subtype from the foundation mapping registry
            subtype = resolveSubtypeFromMapping(instancePath, foundationMappingRegistry);
            if (!subtype) {
                // const fieldName = instancePath.replace(/^\//, "").replace(/\//g, ".");
                // tokenValueContentValidate.errors = [
                //   {
                //     keyword: "tokenValueContent",
                //     message: `Cannot determine subtype for override token at "${fieldName}". No matching entry found in foundation mapping.`
                //   }
                // ];
                return true;
            }
        }
        else {
            // Base mapping token without subtype — not validatable, skip
            return true;
        }
    }
    const value = node.value;
    // Reference values are validated by the references rule
    if (isTokenReference(value))
        return true;
    const result = validateTokenValue(value, subtype, platform ?? PLATFORM.WEB);
    if (!result.valid) {
        const errorObj = result.errorObject;
        const message = errorObj
            ? `Invalid value "${errorObj.value}" for subtype "${errorObj.subtype}". Expected: ${errorObj.expectedFormat}`
            : `Invalid value "${value}" for subtype "${subtype}"`;
        tokenValueContentValidate.errors = [{ keyword: "tokenValueContent", message }];
        return false;
    }
    return true;
}
/*
 * Validates token string values for reference placeholders.
 * Inputs:
 * - _schema: AJV keyword schema value (not used, required by AJV signature)
 * - tokenValue: string token value being validated
 * Output:
 * - Returns true if all references are valid or rule is disabled
 * - Returns false and attaches validation errors if any reference is invalid
 */
function validateTokenReference(_unusedSchema, tokenValue) {
    const { enabledRules, globalTokenRegistry } = this.context;
    validateTokenReference.errors = null;
    if (!enabledRules?.[VALIDATION_RULES.REFERENCES])
        return true;
    const referencePlaceholders = typeof tokenValue === "string" ? tokenValue.match(/{[^}]+}/g) : null;
    if (!referencePlaceholders)
        return true;
    const invalidReference = referencePlaceholders.find(referenceExpression => {
        const rawReferencePath = referenceExpression.replace(/[{}]/g, "")?.trim();
        // Reject empty references
        if (!rawReferencePath) {
            validateTokenReference.errors = [
                {
                    keyword: "refExists",
                    message: `Empty reference is not allowed → ${referenceExpression}`
                }
            ];
            return true;
        }
        // Ensure reference ends with `.value`
        if (!rawReferencePath.endsWith(".value")) {
            validateTokenReference.errors = [
                {
                    keyword: "refExists",
                    message: `Reference must end with .value → ${referenceExpression}`
                }
            ];
            return true;
        }
        const resolvedTokenPath = rawReferencePath.replace(/\.value$/, "");
        // Ensure referenced token exists globally
        return !globalTokenRegistry[resolvedTokenPath];
    });
    if (invalidReference) {
        if (!validateTokenReference.errors) {
            validateTokenReference.errors = [
                {
                    keyword: "refExists",
                    message: `Reference does not exist → ${invalidReference}`
                }
            ];
        }
        return false;
    }
    return true;
}
/*
 * Creates the override validation function used by the overrideContext AJV keyword.
 * Inputs:
 * - _schema: AJV keyword schema value (not used)
 * - _parentSchema: parent schema (not used)
 * Output:
 * - Returns a validator function that AJV executes during validation
 */
function compileOverrideKeyword() {
    return function validateOverrideAgainstBaseMapping(overrideNode, ctx) {
        const { enabledRules, foundationMappingRegistry } = this.context;
        //STOP — do not run override validation on primitives
        if (overrideNode === null || typeof overrideNode !== "object") {
            return true;
        }
        //STOP — do not run override validation on token leaf
        if ("value" in overrideNode) {
            return true;
        }
        // Rule disabled
        if (!enabledRules?.[VALIDATION_RULES.OVERRIDES])
            return true;
        const instancePath = ctx.instancePath;
        const isOverrideRoot = instancePath.endsWith("/mapping") ||
            instancePath.match(/\/states\/[^/]+$/) ||
            instancePath.match(/\/variantGroups\/[^/]+\/[^/]+$/) ||
            instancePath.match(/\/variantGroups\/[^/]+\/[^/]+\/states\/[^/]+$/);
        if (!isOverrideRoot)
            return true;
        const root = ctx.rootData;
        // Component name from override JSON
        const componentName = Object.keys(root)[0];
        // BASE STRUCTURE MUST COME FROM FOUNDATION
        const baseStructure = foundationMappingRegistry[componentName];
        // If foundation has no mapping, skip validation
        if (!baseStructure)
            return true;
        const overrideErrors = [];
        const basePath = deriveBasePath(componentName, instancePath);
        validateOverridePathRecursively(overrideNode, baseStructure, basePath, overrideErrors, instancePath);
        if (overrideErrors.length) {
            validateOverrideAgainstBaseMapping.errors = overrideErrors.map(e => ({
                instancePath: e.instancePath,
                keyword: "overrideContext",
                message: e.message
            }));
            return false;
        }
        return true;
    };
}
// Resolves the root directory of foundation tokens.
function resolveFoundationTokenRootDir(platform = PLATFORM.WEB) {
    if (!Object.values(PLATFORM).includes(platform)) {
        throw new Error("Platform property is invalid, allowed properties are " +
            Object.values(PLATFORM).join(", "));
    }
    // Full path of this file (validate-token.js)
    const currentFilePath = fileURLToPath(import.meta.url);
    // Directory containing this file (scripts/)
    const currentDirPath = path.dirname(currentFilePath);
    const folder = platform == PLATFORM.MOBILE ? FOLDER_TYPE.MOBILE : FOLDER_TYPE.WEB;
    return path.resolve(currentDirPath, "../../src/tokens", folder);
}
/*
 * Validates override paths against the foundation component mapping recursively.
 * Inputs:
 * - overrideNode: override token node being validated
 * - baseNode: corresponding foundation mapping node
 * - path: dot-separated path representing the current base mapping location
 * - overrideErrors: array used to collect override validation errors
 * - instancePath: AJV instance path of the current override node
 * Output:
 * - Populates the overrideErrors array with invalid override path errors
 */
function validateOverridePathRecursively(overrideNode, baseNode, path, overrideErrors, instancePath) {
    if (path.endsWith(".states")) {
        return;
    }
    //stop recursion at token leaf object
    if (overrideNode &&
        typeof overrideNode === "object" &&
        ("value" in overrideNode || "type" in overrideNode || "attributes" in overrideNode)) {
        return;
    }
    // Base missing
    if (!baseNode) {
        overrideErrors.push({
            instancePath,
            message: `Override path does NOT exist in base mapping → ${path}`
        });
        return;
    }
    // Override must be object
    if (overrideNode === null || typeof overrideNode !== "object") {
        return;
    }
    // BASE IS LEAF
    if (baseNode.__leaf) {
        for (const key of Object.keys(overrideNode || {})) {
            if (!["value", "type", "attributes"].includes(key)) {
                overrideErrors.push({
                    instancePath: `${instancePath}/${key}`,
                    message: `Override path does NOT exist in base mapping → ${path}.${key}`
                });
            }
        }
        return;
    }
    for (const key of Object.keys(overrideNode)) {
        // NEVER validate "states" structurally
        if (key === "states") {
            continue;
        }
        if (!baseNode[key]) {
            overrideErrors.push({
                instancePath: `${instancePath}/${key}`,
                message: `Override path does NOT exist in base mapping → ${path}.${key}`
            });
            continue;
        }
        validateOverridePathRecursively(overrideNode[key], baseNode[key], `${path}.${key}`, overrideErrors, `${instancePath}/${key}`);
    }
}
/*
 * Registers all token paths that define a concrete value by traversing a token JSON tree.
 * Inputs:
 * - tokenNode: token JSON object to traverse
 * - parentPath: dot-separated parent token path (used during recursion)
 * Output:
 * - Populates the global token registry with resolved token paths
 */
function registerTokenDefinitionsRecursively(tokenNode, registry, parentPath = "") {
    if (!tokenNode || typeof tokenNode !== "object")
        return;
    Object.keys(tokenNode).forEach(tokenKey => {
        const currentTokenPath = parentPath
            ? `${parentPath}.${tokenKey}`
            : tokenKey;
        const tokenValue = tokenNode[tokenKey];
        // Register tokens that define a concrete value
        if (tokenValue &&
            typeof tokenValue === "object" &&
            "value" in tokenValue) {
            if (!registry[currentTokenPath]) {
                registry[currentTokenPath] = true;
            }
        }
        // Continue traversal for nested token objects
        if (tokenValue && typeof tokenValue === "object") {
            registerTokenDefinitionsRecursively(tokenValue, registry, currentTokenPath);
        }
    });
}
/*
 * Registers all discovered token definitions into the global token registry.
 * Inputs:
 * - directoryPath: root directory to scan for token JSON files
 * Output:
 * - Registers all discovered token definitions into the global token registry
 */
function collectAllTokenDefinitionsFromDir(directoryPath, registry) {
    fs.readdirSync(directoryPath).forEach(entryName => {
        const entryPath = path.join(directoryPath, entryName);
        const entryStats = fs.statSync(entryPath);
        // Recurse into subdirectories
        if (entryStats.isDirectory()) {
            return collectAllTokenDefinitionsFromDir(entryPath, registry);
        }
        // Process only JSON token files
        if (!entryName.endsWith(".json"))
            return;
        const tokenFileContent = JSON.parse(fs.readFileSync(entryPath, "utf8"));
        registerTokenDefinitionsRecursively(tokenFileContent, registry);
    });
}
//Builds the foundation global tokens directory path and passes it for token definition collection.
function collectFoundationGlobalTokens(foundationRootDir) {
    const registry = {};
    const globalDir = path.join(foundationRootDir, "global");
    collectAllTokenDefinitionsFromDir(globalDir, registry);
    return registry;
}
//Builds the foundation components directory path and collects component mapping structures for override validation.
function collectFoundationComponentMappings(foundationRootDir) {
    const registry = {};
    const componentsDir = path.join(foundationRootDir, "components");
    collectFoundationMappings(componentsDir, registry);
    return registry;
}
/*
 * Determines which properties are allowed for a given JSON path.
 * Inputs:
 * - instancePath: AJV instance path of the current validation error
 * Output:
 * - Returns a list of allowed property names for the path
 * - Returns undefined if no specific rule applies
 */
function resolveAllowedProperties(instancePath) {
    const normalizedPath = instancePath
        ? instancePath.replace(/^\//, "").replace(/\//g, ".")
        : "";
    for (const rule of ALLOWED_PROPERTIES_BY_CONTEXT) {
        if (rule.match(normalizedPath)) {
            return rule.allowed;
        }
    }
    return undefined;
}
/*
 * Resolves which validation rules are enabled for the current validation run.
 * Inputs: - validationOptions: configuration that may specify enabled validation rules
 * Output: - Returns an object mapping each validation rule to a boolean flag
 */
function resolveEnabledValidationRules({ rules = [], isFoundationValidation = false } = {}) {
    const allAvailableRules = Object.values(VALIDATION_RULES);
    // Define rules when no rules are set explicitly 
    const defaultEnabledRules = new Set([VALIDATION_RULES.STRUCTURE, VALIDATION_RULES.METADATA]);
    return Object.fromEntries(allAvailableRules.map(rule => {
        if (isFoundationValidation) {
            //Excluding reference rule for foundation css
            if (rule === VALIDATION_RULES.REFERENCES) {
                return [rule, false];
            }
            return [rule, true];
        }
        //If rules set explicity then enable only those rules
        if (rules.length > 0)
            return [rule, rules.includes(rule)];
        //Enable only if NOT in the default enabled set of rules for component validation
        return [rule, !defaultEnabledRules.has(rule)];
    }));
}
/*
  * Processes AJV validation errors and converts them into normalized validation errors.
  * Inputs:
  * - validate: compiled AJV validation function containing validation errors
  * - filePath: path of the JSON file being validated (optional)
*/
function handleValidationErrors({ validate, filePath }, validationErrors) {
    const fileName = filePath ? filePath.split("/").pop() : undefined;
    if (validate.errors) {
        validate.errors.forEach(ajvError => {
            const { fieldName, errorMessage, errorMetadata } = extractValidationErrorDetails(ajvError);
            // Default error object
            let errorObj = {
                errorType: ERROR_TYPES.INVALID_VALUE,
                fieldName,
                message: errorMessage,
                fileName
            };
            switch (ajvError.keyword) {
                // RULE-3 → Reference errors
                case "refExists":
                    errorObj.errorType = ERROR_TYPES.INVALID_REFERENCE;
                    break;
                // RULE-4 → Invalid override fields
                case "overrideContext":
                    errorObj.errorType = ERROR_TYPES.INVALID_FIELD;
                    break;
                // Leaf / enum value validation
                case "leafMustHaveValue":
                case "enum":
                    if (errorMetadata.allowedValues) {
                        errorObj.allowedValues = errorMetadata.allowedValues;
                    }
                    break;
                // TYPE errors → structure or value
                case "type": {
                    const expectedType = ajvError.params?.type;
                    const actualType = Array.isArray(ajvError.data)
                        ? "array"
                        : typeof ajvError.data;
                    if (expectedType === "object") {
                        errorObj.errorType = ERROR_TYPES.INVALID_STRUCTURE;
                        errorObj.message = `Expected a token object, but found ${actualType}`;
                    }
                    break;
                }
                // JSON structure issues
                case "required":
                case "minProperties":
                case "maxProperties":
                    errorObj.errorType = ERROR_TYPES.INVALID_STRUCTURE;
                    break;
                case "additionalProperties":
                    errorObj.errorType = ERROR_TYPES.INVALID_STRUCTURE;
                    const allowedValues = resolveAllowedProperties(ajvError.instancePath);
                    if (allowedValues) {
                        errorObj.allowedValues = allowedValues;
                    }
                    break;
                default:
                    // RULE-2 → Missing metadata
                    if (errorMessage.includes('must match "then" schema')) {
                        errorObj.errorType = ERROR_TYPES.MISSING_FIELD;
                        errorObj.message =
                            "Missing required metadata: type, attributes.subtype, attributes.description";
                    }
            }
            if (VALUE_RELATED_KEYWORDS.has(ajvError.keyword)) {
                errorObj.actualValue = ajvError.data;
            }
            pushError(errorObj, validationErrors);
        });
    }
}
/*
 * Extracts readable validation error details from an AJV error object.
 *
 * Input:
 * - ajvError: a single validation error produced by AJV
 *
 * Output:
 * - fieldName: dot-separated path of the invalid field
 * - errorMessage: human-readable validation message
 * - errorMetadata: additional details such as allowed values (when applicable)
 */
function extractValidationErrorDetails(ajvError) {
    const fieldName = ajvError.instancePath
        ? ajvError.instancePath.replace(/^\//, "").replace(/\//g, ".")
        : "(root)";
    const errorMessage = ajvError.message || "Invalid token value";
    const errorMetadata = {};
    // ENUM-specific metadata
    if (ajvError.keyword === "enum" && Array.isArray(ajvError.params?.allowedValues)) {
        errorMetadata.allowedValues = ajvError.params.allowedValues;
    }
    return {
        fieldName,
        errorMessage,
        errorMetadata
    };
}
//Validates the token type against supported values and normalizes it.
function validateAndNormalizeTokenType(tokenType) {
    if (typeof tokenType !== "string") {
        throw new Error(`tokenType must be a string`);
    }
    const normalizedTokenType = tokenType.toLowerCase();
    if (!TOKEN_TYPE_CONFIG[normalizedTokenType]) {
        throw new Error(`Invalid tokenType "${tokenType}". Allowed values: ${Object.keys(TOKEN_TYPE_CONFIG).join(", ")}`);
    }
    return normalizedTokenType;
}
/*
 * Collects structural mapping definitions for all foundation components.
 * Input:
 * - directoryPath: root directory containing foundation component token files
 *
 * Output:
 * - Populates the foundation mapping registry with structure-only representations of each component’s mapping
 */
function collectFoundationMappings(directoryPath, componentsMappingregistry = {}) {
    // Read all entries(files + folders ) in the directory
    fs.readdirSync(directoryPath).forEach(entryName => {
        // Absolute path to the current entry
        const entryPath = path.join(directoryPath, entryName);
        // File system metadata (file or directory)
        const entryStats = fs.statSync(entryPath);
        // Recurse into subdirectories
        if (entryStats.isDirectory()) {
            collectFoundationMappings(entryPath, componentsMappingregistry);
            return;
        }
        if (!entryName.endsWith(".json"))
            return;
        const componentJson = JSON.parse(fs.readFileSync(entryPath, "utf8"));
        for (const componentName of Object.keys(componentJson)) {
            const mapping = componentJson[componentName]?.mapping;
            if (!mapping)
                continue;
            componentsMappingregistry[componentName] = extractMappingStructure(mapping);
        }
    });
    return componentsMappingregistry;
}
/*
 * Extracts the structural shape of a foundation component mapping for override validation.
 *
 * Input:
 * - componentMapping / node: the `mapping` object of a foundation component token
 *
 * Output:
 * - Returns a structure-only representation of the mapping
 * - Leaf nodes are marked to indicate valid override endpoints
 */
function extractMappingStructure(node) {
    if (!node || typeof node !== "object") {
        return { __leaf: true };
    }
    const keys = Object.keys(node).filter(k => !["value", "type", "attributes", "states"].includes(k));
    // LEAF TOKEN
    if (keys.length === 0) {
        const leaf = { __leaf: true };
        if (node.attributes?.subtype)
            leaf.__subtype = node.attributes.subtype;
        return leaf;
    }
    const structure = {};
    for (const key of keys) {
        structure[key] = extractMappingStructure(node[key]);
    }
    return structure;
}
//Finds the correct starting point for validating an override.
function deriveBasePath(componentName, instancePath) {
    if (instancePath.includes("/variantGroups")) {
        return `${componentName}.variantGroups`;
    }
    if (instancePath.includes("/states")) {
        return `${componentName}.states`;
    }
    return `${componentName}.mapping`;
}
/*
 * Collects token definitions from an array of JSON objects.
 * Inputs: - jsonArray: an array of token JSON objects
 * Output: Registers all token paths that define a concrete value into the internal global token registry
 */
function collectTokenDefinitionsFromJsonArray(registry, jsonArray = []) {
    if (!Array.isArray(jsonArray))
        return;
    jsonArray.forEach((json) => {
        if (!json || typeof json !== "object")
            return;
        registerTokenDefinitionsRecursively(json, registry);
    });
}
/*
 * Validates a single token JSON object (global or component).
 * Inputs:
 * - json: The token JSON object to validate.
 * - tokenType: The type of token being validated ("global" or "component").
 * - validationConfig:Controls which validation rules are enabled for this run.
 *   - rules: optional list of rule names to enable explicitly
 *   - isFoundationValidation: when true, all rules are enabled
 * - externalGlobalTokenJsons: Optional additional global token JSONs whose token values can be referenced during validation.
 * Output:Validation errors are collected internally and can be retrieved using getValidationErrors().
 * - In the next iteration, project directory overrides will be used instead of externalGlobalTokenJsons.
 */
export class TokenValidator {
    constructor(args) {
        this.externalGlobalTokenJsons = args.externalGlobalTokenJsons || [];
        this.validationErrors = [];
        this.ajvEngine = new TokenAjvEngine();
        this.ajv = this.ajvEngine.getValidatorEngine();
        this.platform = args.platform ?? PLATFORM.WEB;
        this.foundationTokensDir = resolveFoundationTokenRootDir(this.platform);
        const globalTokenRegistry = collectFoundationGlobalTokens(this.foundationTokensDir);
        if (Array.isArray(this.externalGlobalTokenJsons)) {
            collectTokenDefinitionsFromJsonArray(globalTokenRegistry, this.externalGlobalTokenJsons);
        }
        this.ajv.context.globalTokenRegistry = globalTokenRegistry;
        this.ajv.context.platform = this.platform;
    }
    // Validates a token JSON object against the appropriate schema and rules based on its type.
    validateTokenJson({ json, validationConfig = {}, tokenType }) {
        if (!json || typeof json !== "object") {
            throw new Error("validateTokenJson requires a valid JSON object");
        }
        // Reset validation errors for each run
        this.validationErrors.length = 0;
        const resolvedTokenType = validateAndNormalizeTokenType(tokenType);
        const tokenConfig = TOKEN_TYPE_CONFIG[resolvedTokenType];
        this.ajv.context.enabledRules = tokenConfig.resolveRules(validationConfig);
        this.ajv.context.foundationMappingRegistry = {};
        if (tokenType === TOKEN_TYPES.COMPONENT) {
            this.ajv.context.foundationMappingRegistry = collectFoundationComponentMappings(this.foundationTokensDir);
        }
        const schema = tokenConfig.getSchema(this.ajv.context.enabledRules);
        const validate = this.ajv.compile(schema);
        validate(json);
        handleValidationErrors({ validate }, this.validationErrors);
    }
    getValidationErrors() {
        return this.validationErrors;
    }
}
// Custom AJV engine class to encapsulate validator instance and custom keyword registrations
class TokenAjvEngine {
    constructor() {
        this.validatorEngine = new Ajv({ allErrors: true, verbose: true, allowMatchingProperties: true });
        this.validatorEngine.context = {
            enabledRules: null,
            globalTokenRegistry: {},
            foundationMappingRegistry: {},
            platform: PLATFORM.WEB
        };
        this.registerCustomValidations();
    }
    registerCustomValidations() {
        const CUSTOM_VALIDATIONS = [
            {
                keyword: "refExists",
                type: "string",
                errors: true,
                validate: validateTokenReference
            },
            {
                keyword: "overrideContext",
                type: "object",
                errors: true,
                compile: compileOverrideKeyword
            },
            {
                keyword: "leafMustHaveValue",
                type: ["object", "string"],
                errors: true,
                validate: leafMustHaveValueValidate
            },
            {
                keyword: "tokenValueContent",
                type: "object",
                errors: true,
                validate: tokenValueContentValidate
            }
        ];
        CUSTOM_VALIDATIONS.forEach(k => this.validatorEngine.addKeyword(k));
    }
    getValidatorEngine() {
        return this.validatorEngine;
    }
}
/*
  Validates an entire folder hierarchy of token JSON files.
  * Constructor inputs:
    * 1) validationScope:
    *    - Determines the validation mode.
    *    - Possible values:
    *      - VALIDATION_SCOPE.FOUNDATION → validates foundation CSS tokens
    *      - VALIDATION_SCOPE.PROJECT → validates project override tokens
    *
    * 2) projectOverrideDirs - Exact directories where project token validation should run.
    *
    * 3) isWebFolder:
    *    - Indicates whether validation should target the web or mobile
    *      foundation token folder.
    *    - Used to resolve the correct foundation token root directory.
    *
*/
export class TokenFolderValidator {
    constructor(args) {
        this.validationScope = args.validationScope || VALIDATION_SCOPE.PROJECT;
        this.projectOverrideDirs = args.projectOverrideDirs || [];
        this.validationErrors = [];
        this.ajvEngine = new TokenAjvEngine();
        this.ajv = this.ajvEngine.getValidatorEngine();
        this.platform = args.platform ?? PLATFORM.WEB;
        this.foundationTokensDir = resolveFoundationTokenRootDir(this.platform);
        this.createAjvContext();
        this.resolvedValidationTargets = this.resolveValidationTargets();
    }
    createAjvContext() {
        this.isFoundation = this.validationScope === VALIDATION_SCOPE.FOUNDATION;
        this.isProject = this.validationScope === VALIDATION_SCOPE.PROJECT;
        this.ajv.context.platform = this.platform;
        if (this.isProject && this.projectOverrideDirs.length === 0) {
            pushError({
                errorType: ERROR_TYPES.BUILD_ERROR,
                fieldName: "Root",
                message: "Project validation requires at least one project override directory, but none were provided."
            }, this.validationErrors);
            return;
        }
        this.ajv.context.foundationMappingRegistry = collectFoundationComponentMappings(this.foundationTokensDir);
        const globalTokenRegistry = collectFoundationGlobalTokens(this.foundationTokensDir);
        if (this.isProject) {
            this.projectOverrideDirs.forEach(projectDir => {
                const { globalDir } = this.resolveProjectOverrideDirs(projectDir);
                if (globalDir && fs.existsSync(globalDir)) {
                    collectAllTokenDefinitionsFromDir(globalDir, globalTokenRegistry);
                }
            });
        }
        this.ajv.context.globalTokenRegistry = globalTokenRegistry;
    }
    // Resolves which folders should be validated.
    resolveValidationTargets() {
        const validationTargets = [];
        if (this.isProject) {
            this.projectOverrideDirs.forEach(projectDir => {
                if (!fs.existsSync(projectDir))
                    return;
                const { globalDir, componentsDir } = this.resolveProjectOverrideDirs(projectDir);
                if (globalDir && fs.existsSync(globalDir)) {
                    validationTargets.push({ type: TOKEN_TYPES.GLOBAL, dir: globalDir });
                }
                if (componentsDir && fs.existsSync(componentsDir)) {
                    validationTargets.push({ type: TOKEN_TYPES.COMPONENT, dir: componentsDir });
                }
            });
        }
        //Resolve folders to validate
        if (this.isFoundation) {
            const globalDir = path.join(this.foundationTokensDir, "global");
            const componentsDir = path.join(this.foundationTokensDir, "components");
            validationTargets.push({ type: TOKEN_TYPES.GLOBAL, dir: globalDir });
            validationTargets.push({ type: TOKEN_TYPES.COMPONENT, dir: componentsDir });
        }
        return validationTargets;
    }
    /* Main entry point that validates all token JSON files in the resolved folders
    Inputs :
    1) validationConfig:Controls which validation rules are enabled for this run.
    *   - rules: optional list of rule names to enable explicitly
    *   - isFoundationValidation: when true, all rules are enabled
    */
    validateFolder(validationConfig = {}) {
        // Nothing to validate
        if (this.isProject && this.projectOverrideDirs.length === 0) {
            return;
        }
        // Clear previous errors
        this.validationErrors.length = 0;
        this.resolvedValidationTargets.forEach(({ type, dir }) => {
            const tokenConfig = TOKEN_TYPE_CONFIG[type];
            // Enable rules for this run
            this.ajv.context.enabledRules = tokenConfig.resolveRules({
                ...validationConfig,
                isFoundationValidation: this.isFoundation
            });
            const schema = tokenConfig.getSchema(this.ajv.context.enabledRules);
            const validate = this.ajv.compile(schema);
            this.traverseAndValidateJsonFiles(dir, validate);
        });
    }
    // Returns all collected validation errors.
    getValidationErrors() {
        return this.validationErrors;
    }
    //Resolves the available global and component override directories for a project.
    resolveProjectOverrideDirs(overridesDir) {
        const globalDir = path.join(overridesDir, "global");
        const componentsDir = path.join(overridesDir, "components");
        return {
            globalDir: fs.existsSync(globalDir) ? globalDir : null,
            componentsDir: fs.existsSync(componentsDir) ? componentsDir : null
        };
    }
    /*
    * Validates a single JSON file using schema and custom validation rules.
    * Inputs:
    * - filePath: path to the JSON file being validated
    * - validate: compiled AJV validation function
    * Output:
    * - Parses and validates the file
    * - Collects validation errors if any are found
    */
    validateFile(filePath, validate) {
        const { ok, json, error } = this.parseJsonSafely(filePath);
        const file = path.basename(filePath);
        //JSON STRUCTURE VALIDATION
        if (!ok) {
            pushError({
                errorType: ERROR_TYPES.INVALID_JSON,
                fieldName: "(root)",
                message: error.message,
                fileName: file
            });
            return; // don't run AJV
        }
        validate(json);
        handleValidationErrors({
            validate,
            filePath
        }, this.validationErrors);
    }
    /*
    * Safely reads and parses a JSON file without throwing runtime errors.
    * Inputs: filePath: path to the JSON file
    * Output: Returns an object with either the parsed JSON or the parsing error
    */
    parseJsonSafely(filePath) {
        try {
            const content = fs.readFileSync(filePath, "utf8");
            return { ok: true, json: JSON.parse(content) };
        }
        catch (err) {
            return { ok: false, error: err };
        }
    }
    /*
    * Recursively traverses a directory and validates all JSON files found.
    * Inputs:
    * - dir: root directory to traverse
    * - validate: compiled AJV validation function
    * Output: - Validates each JSON file and collects validation errors
    */
    traverseAndValidateJsonFiles(dir, validate, visitedSet = new Set()) {
        // Resolve the real path of the current directory
        const realDir = fs.realpathSync(dir);
        // If we’ve already visited this path, skip it to avoid cycles
        if (visitedSet.has(realDir))
            return;
        visitedSet.add(realDir);
        // Read directory contents
        fs.readdirSync(dir).forEach(name => {
            const full = path.join(dir, name);
            let stat;
            try {
                stat = fs.statSync(full);
            }
            catch (err) {
                console.warn(`Skipping inaccessible file: ${full}`);
                return;
            }
            if (stat.isDirectory()) {
                this.traverseAndValidateJsonFiles(full, validate, visitedSet);
            }
            else if (name.endsWith(".json")) {
                this.validateFile(full, validate);
            }
        });
    }
    /**
   * Generates a Markdown validation report summarizing all errors.
   * Output:
   * - Creates a README-style markdown file listing validation errors in generated folder
   */
    generateValidationReport(outputDir) {
        // Default to <project-root>/generated if outputDir is not provided
        const baseDir = outputDir || path.join(process.cwd(), "generated");
        // Ensure folder exists
        if (!fs.existsSync(baseDir)) {
            fs.mkdirSync(baseDir, { recursive: true });
        }
        if (!this.validationErrors.length) {
            const content = `# Token Validation Report\n\n No validation errors found.\n`;
            fs.writeFileSync(path.join(baseDir, "VALIDATION_REPORT.md"), content);
            return;
        }
        const groupedByFile = {};
        // Group errors by file
        this.validationErrors.forEach(err => {
            const file = err.fileName || "(unknown file)";
            if (!groupedByFile[file])
                groupedByFile[file] = [];
            groupedByFile[file].push(err);
        });
        let md = `# Token Validation Report\n\n`;
        md += `Generated on: ${new Date().toLocaleString()}\n\n`;
        md += `Total Errors: **${this.validationErrors.length}**\n\n`;
        md += `---\n`;
        Object.entries(groupedByFile).forEach(([file, errors]) => {
            md += `## ${file} – ${errors.length} error${errors.length > 1 ? "s" : ""}\n\n`;
            errors.forEach((err, index) => {
                md += `### ${index + 1}. ${err.errorType}\n`;
                md += `- **Path**: \`${err.fieldName}\`\n`;
                md += `- **Message**: ${err.message}\n`;
                if (err.allowedValues) {
                    md += `- **Allowed Values**: ${err.allowedValues.join(", ")}\n`;
                }
                if (err.actualValue) {
                    md += `- **Actual value**: ${err.actualValue}\n `;
                }
                md += `\n`;
            });
            md += `---\n`;
        });
        const reportName = this.platform === PLATFORM.MOBILE
            ? "mobile_tokens_validation_report.md"
            : "web_tokens_validation_report.md";
        fs.writeFileSync(path.join(baseDir, reportName), md, "utf8");
    }
}
//# sourceMappingURL=validate-token.js.map