import { existsSync } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import Ajv2020Module from "ajv/dist/2020.js";
import addFormatsModule from "ajv-formats";
import { CLI_VALIDATED_REGISTRY } from "./registry.js";
const ENV_PLACEHOLDER_PATTERN = /\$\{([A-Z0-9_]+)(?::([^}]*))?\}/g;
async function discoverTenantDirectories(rootPath) {
    const entries = await readdir(rootPath, { withFileTypes: true });
    return entries
        .filter((entry) => entry.isDirectory())
        .map((entry) => entry.name)
        .sort((left, right) => left.localeCompare(right));
}
export async function validateTenantsRoot(rootPath, schemas, env) {
    if (!existsSync(rootPath)) {
        throw new Error(`Configuration root not found: ${rootPath}`);
    }
    const tenantIds = await discoverTenantDirectories(rootPath);
    const results = [];
    for (const tenantId of tenantIds) {
        results.push(await validateTenantDirectory(join(rootPath, tenantId), tenantId, schemas, env));
    }
    return results;
}
export async function validateTenantDirectory(tenantPath, tenantId, schemas, env) {
    if (!existsSync(tenantPath)) {
        throw new Error(`Tenant directory not found: ${tenantPath}`);
    }
    const state = {
        getValidator: createValidatorFactory(schemas),
        env,
        errors: [],
        resourceCounts: {},
        files: 0,
    };
    for (const entry of CLI_VALIDATED_REGISTRY) {
        if (entry.kind === "file") {
            await validateFixedFileEntry(tenantPath, entry, state);
        }
        else {
            await validateDirectoryEntry(tenantPath, entry, state);
        }
    }
    return {
        id: tenantId,
        valid: state.errors.length === 0,
        files: state.files,
        errors: state.errors,
        resourceCounts: state.resourceCounts,
    };
}
function createValidatorFactory(schemas) {
    const ajv = new Ajv2020Module.default({ allErrors: true, strict: false });
    addFormatsModule.default(ajv);
    const validators = new Map();
    return (schemaFile) => {
        const cached = validators.get(schemaFile);
        if (cached) {
            return cached;
        }
        const schema = schemas.get(schemaFile);
        if (!schema) {
            throw new Error(`No bundled schema found for "${schemaFile}".`);
        }
        const validator = ajv.compile(schema);
        validators.set(schemaFile, validator);
        return validator;
    };
}
async function validateFixedFileEntry(tenantPath, entry, state) {
    const filePath = join(tenantPath, entry.file);
    if (!existsSync(filePath)) {
        if (entry.required) {
            state.errors.push({
                file: entry.file,
                message: `Missing required ${entry.resourceType} file`,
            });
        }
        return;
    }
    state.files += 1;
    await recordValidationOutcome(filePath, entry.file, entry.resourceType, entry.schemaFile, state);
}
async function validateDirectoryEntry(tenantPath, entry, state) {
    const directoryPath = join(tenantPath, entry.subfolder);
    if (!existsSync(directoryPath)) {
        return;
    }
    const dirEntries = (await readdir(directoryPath, { withFileTypes: true }))
        .filter((dirEntry) => dirEntry.isFile() && dirEntry.name.endsWith(".json"))
        .sort((left, right) => left.name.localeCompare(right.name));
    for (const dirEntry of dirEntries) {
        const relativeFile = `${entry.subfolder}/${dirEntry.name}`;
        const filePath = join(directoryPath, dirEntry.name);
        state.files += 1;
        await recordValidationOutcome(filePath, relativeFile, entry.resourceType, entry.schemaFile, state);
    }
}
async function recordValidationOutcome(filePath, relativeFile, resourceType, schemaFile, state) {
    const isValid = await validateResourceFile(filePath, relativeFile, schemaFile, state.getValidator, state.env, state.errors);
    if (isValid) {
        state.resourceCounts[resourceType] = (state.resourceCounts[resourceType] ?? 0) + 1;
    }
}
async function validateResourceFile(filePath, relativeFile, schemaFile, getValidator, env, errors) {
    let raw;
    try {
        raw = await readFile(filePath, "utf8");
    }
    catch (error) {
        errors.push({
            file: relativeFile,
            message: `Failed to read file: ${error.message}`,
        });
        return false;
    }
    let payload;
    try {
        payload = JSON.parse(raw);
    }
    catch (error) {
        errors.push({ file: relativeFile, message: `Invalid JSON: ${error.message}` });
        return false;
    }
    const errorsBefore = errors.length;
    const resolved = resolvePlaceholders(payload, env, relativeFile, errors);
    if (errors.length > errorsBefore) {
        return false;
    }
    const validate = getValidator(schemaFile);
    if (!validate(resolved)) {
        for (const issue of validate.errors ?? []) {
            errors.push({
                file: relativeFile,
                path: formatErrorPath(issue),
                message: issue.message ?? "Invalid value",
            });
        }
        return false;
    }
    return true;
}
function formatErrorPath(issue) {
    const path = issue.instancePath.replace(/^\//, "").replaceAll("/", ".");
    if (path) {
        return path;
    }
    const missingProperty = issue.params
        ?.missingProperty;
    return missingProperty;
}
/**
 * Resolves ${VAR} / ${VAR:default} placeholders for schema validation without ever
 * emitting resolved secret values in error output; unresolved required placeholders
 * are reported by reference to their variable name only.
 */
function resolvePlaceholders(value, env, file, errors) {
    if (typeof value === "string") {
        return value.replace(ENV_PLACEHOLDER_PATTERN, (match, varName, defaultValue) => {
            const envValue = env[varName];
            if (envValue !== undefined && envValue !== "") {
                return envValue;
            }
            if (defaultValue !== undefined) {
                return defaultValue;
            }
            errors.push({
                file,
                message: `Unresolved placeholder \${${varName}}: no environment value or default is available`,
            });
            return match;
        });
    }
    if (Array.isArray(value)) {
        return value.map((item) => resolvePlaceholders(item, env, file, errors));
    }
    if (value && typeof value === "object") {
        const result = {};
        for (const [key, val] of Object.entries(value)) {
            result[key] = resolvePlaceholders(val, env, file, errors);
        }
        return result;
    }
    return value;
}
//# sourceMappingURL=validator.js.map